DANIEL.RUIZ_

/ personal lab / iac

Infrastructure as Code

Identity and app access live in Terraform: Authentik is the IdP, Google Workspace signs in over SAML, and self-hosted apps sit behind forward-proxy auth. Cloudflare owns the edge. Every change goes through a pull request, and secrets never touch git.

01 / LAYOUT

Managed files

One file per integration pattern, so a diff shows which kind of access changed.

FileRole
google_workspace.tfGoogle Workspace SAML
proxy_apps.tfForward-proxy apps: Uptime Kuma, n8n, Prowlarr, Obsidian
slack.tfSlack link-only tile
reputation.tfReputation / access policy
02 / PATTERN

Integration picker

  • OIDC
  • SAML
  • Proxy
  • Link-only
  • Local-only (WARP)

Pick the pattern the app actually supports, not the one that is easiest to click through. Native OIDC or SAML first; forward-proxy auth when the app has no SSO of its own; a link-only tile when the vendor owns sign-in; and no public hostname at all when the app only needs to be reachable from my own devices.

03 / EXAMPLES

Worked examples

Sanitized HCL from the authentik provider. IDs, URLs, and hostnames are placeholders; real values come from variables that are never committed.

SAML — Google Workspace

# google_workspace.tf
resource "authentik_provider_saml" "google_workspace" {
  name               = "google-workspace"
  authorization_flow = data.authentik_flow.default_authorization.id
  invalidation_flow  = data.authentik_flow.default_invalidation.id
  acs_url            = var.google_acs_url   # "https://…/acs"
  audience           = var.google_entity_id # "google-workspace-entity"
  sp_binding         = "post"
  signing_kp         = data.authentik_certificate_key_pair.saml.id
}

resource "authentik_application" "google_workspace" {
  name              = "Google Workspace"
  slug              = "google-workspace"
  protocol_provider = authentik_provider_saml.google_workspace.id
}

OIDC — generic pattern

resource "authentik_provider_oauth2" "example" {
  name               = "example-app"
  client_id          = var.example_client_id
  client_type        = "confidential"
  authorization_flow = data.authentik_flow.default_authorization.id
  invalidation_flow  = data.authentik_flow.default_invalidation.id
  allowed_redirect_uris = [
    { matching_mode = "strict", url = "https://app.example.com/oauth/callback" },
  ]
}
# The client secret is generated by authentik and handed to the app
# out of band. It never appears in .tf files, tfvars, or PRs.

Proxy — one map, many apps

# proxy_apps.tf
locals {
  proxy_apps = {
    uptime-kuma = { name = "Uptime Kuma", host = "status.example.internal" }
    n8n         = { name = "n8n",         host = "n8n.example.internal" }
    prowlarr    = { name = "Prowlarr",    host = "prowlarr.example.internal" }
    obsidian    = { name = "Obsidian",    host = "notes.example.internal" }
  }
}

resource "authentik_provider_proxy" "app" {
  for_each           = local.proxy_apps
  name               = each.key
  mode               = "forward_single"
  external_host      = "https://${each.value.host}"
  authorization_flow = data.authentik_flow.default_authorization.id
  invalidation_flow  = data.authentik_flow.default_invalidation.id
}

resource "authentik_application" "proxy" {
  for_each          = local.proxy_apps
  name              = each.value.name
  slug              = each.key
  protocol_provider = authentik_provider_proxy.app[each.key].id
}

Adding an app is one line in the map; the plan shows exactly one provider and one application being created.

Link-only — Slack

# slack.tf
resource "authentik_application" "slack" {
  name            = "Slack"
  slug            = "slack"
  meta_launch_url = "https://example.slack.com"
  open_in_new_tab = true
  # No protocol_provider: a launcher tile, not an SSO integration.
}

Reputation policy

# reputation.tf
resource "authentik_policy_reputation" "login_guard" {
  name           = "login-reputation-guard"
  check_ip       = true
  check_username = true
  threshold      = -5 # repeated failures fall below this and get denied
}

resource "authentik_policy_binding" "login_guard" {
  target = data.authentik_flow.default_authentication.id
  policy = authentik_policy_reputation.login_guard.id
  order  = 0
}
04 / EDGE

Edge access

App typePath in
SSO appsPublic hostname through Cloudflare Tunnel, gated by Cloudflare Access with Authentik upstream. No inbound ports open.
Local-only appsNo public hostname at all. Reached over Cloudflare One / WARP from enrolled devices only.
05 / CHANGE CONTROL

Merge is the approval

  1. branch
  2. pull request
  3. runner: fmt · validate · plan
  4. Grok Bot + Copilot review
  5. human merge = apply

A self-hosted runner formats, validates, and plans every PR so reviewers read the real diff against live state. AI reviewers catch drift and mistakes early; only my merge triggers apply.

← Personal Lab