/ 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.
Managed files
One file per integration pattern, so a diff shows which kind of access changed.
| File | Role |
|---|---|
| google_workspace.tf | Google Workspace SAML |
| proxy_apps.tf | Forward-proxy apps: Uptime Kuma, n8n, Prowlarr, Obsidian |
| slack.tf | Slack link-only tile |
| reputation.tf | Reputation / access policy |
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.
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
}
Edge access
| App type | Path in |
|---|---|
| SSO apps | Public hostname through Cloudflare Tunnel, gated by Cloudflare Access with Authentik upstream. No inbound ports open. |
| Local-only apps | No public hostname at all. Reached over Cloudflare One / WARP from enrolled devices only. |
Merge is the approval
- branch
- pull request
- runner: fmt · validate · plan
- Grok Bot + Copilot review
- 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.