Authentik OAuth2 with Terraform (Day 35)

Authentik OAuth2 with Terraform (Day 35)
Photo by THLT LCX / Unsplash

I recently started using Authentik to provide auth for my services and applications in the homelab.

Authentik is an open-source identity provider that supports OAuth2, SAML, and more, and comes with a Terraform provider, so naturally, I defaulted to managing everything that way.

This means I no longer need to deal with multiple logins. Authentik acts as a single sign-on solution, letting me authenticate once and access everything.

See https://goauthentik.io/ for more info.

Set up

Create a token on Authentik's console under "Tokens and App passwords" and use it to set up the terraform provider

provider "authentik" {
  url   = "<the authentik url>"
  token = "<api token>" 
}
Remember to use something like sops to encrypt your token / secrets.

Get the existing flows that Authentik provides out of the box:

data "authentik_flow" "default-authorization-flow" {
  slug = "default-provider-authorization-explicit-consent"
}

data "authentik_flow" "default-invalidation-flow" {
  slug = "default-provider-invalidation-flow"
}

These flows handle the authorization process and session invalidation. One can still create other flows, but there are plenty of preconfigured flows that just work.

Creating OAuth2 providers

Then create the authentik_provider_oauth2 resource for each application that needs and supports it.

resource "authentik_provider_oauth2" "this" {
  for_each               = var.authentik_application
  name                   = each.key
  client_id              = random_string.client_id[each.key].id
  client_secret          = random_password.client_secret[each.key].result
  authorization_flow     = data.authentik_flow.default-authorization-flow.id
  invalidation_flow      = data.authentik_flow.default-invalidation-flow.id
  refresh_token_validity = var.refresh_token_validity
  allowed_redirect_uris  = each.value.allowed_redirect_uris
  property_mappings      = var.property_mappings
  sub_mode               = var.sub_mode
}

Adding access policies

Add expression policies and bind them to the application. expression policies control who can access what, and are Python expressions that evaluate to true or false:

resource "authentik_policy_expression" "policy" {
  name       = var.policy_expression.name
  expression = var.policy_expression.expression
}

resource "authentik_policy_binding" "app-access" {
  for_each = var.authentik_application
  target   = authentik_application.this[each.key].uuid
  policy   = authentik_policy_expression.policy.id
  order    = 0
}

Creating applications

The applications themselves are straightforward and tie everything together:

resource "authentik_application" "this" {
  for_each          = var.authentik_application
  name              = try(each.value.name, each.key)
  slug              = each.key
  meta_icon         = var.app_meta_icon
  protocol_provider = authentik_provider_oauth2.this[each.key].id
}

Property mappings

These tripped me up initially. They define what user information gets passed to the application during authentication:

variable "property_mappings" {
  # authentik default OAuth Mapping: OpenID 'email' 
  # authentik default OAuth Mapping: OpenID 'openid 
  # authentik default OAuth Mapping: OpenID 'profile'
  default = [
    "4c94fd1d-1655-498f-94dc-e3be8506e0ec",
    "8bb80d61-1994-4538-9942-633b45ecd879",
    "660390cb-184a-4260-a4f0-7d69488a3037",
  ]
}
These UUIDs correspond to the default mappings in Authentik. Without them, authentication might work, but e.g, in Proxmox you may get a lot of 401. I suppose that's because it wasn't receiving the required user info.

Next Steps

  • Integrating more services and maybe exploring SAML for applications that don't play nice with OAuth2.
  • Figure out how to get Cilium ingress and Authentik working together