Replacing Root Tokens with SSO: HashiCorp Vault + Authentik OIDC
If you’re using Vault’s root token for daily operations, you’re doing it wrong. I was too.
Then I accidentally committed my .vault-token file to Gitlab.
The Problem
My original Vault workflow:
export VAULT_TOKEN="hvs.CAESIF..."
vault kv get secret/my-app
Why this is terrible:
- ❌ Root token never expires (unlimited attack window)
- ❌ No audit trail (all operations appear as “root”)
- ❌ Single point of failure (anyone with this token has god-mode)
- ❌ No MFA (compromised laptop = compromised infrastructure)
- ❌ Can’t revoke selectively (revoking root token locks everyone out)
The wake-up call: I accidentally committed .vault-token to Gitlab. Panic-revoking the root token meant:
- Regenerating via Terraform
- Updating every automation script
- Breaking every service that depended on Vault (all of them)
The Solution
Replace root tokens with federated authentication. Users log in once through Authentik SSO, and Vault grants permissions based on group membership.
Architecture
User/Script
│
▼
HashiCorp Vault (OIDC Client)
│
│ Redirect
▼
Authentik (OIDC Provider)
│
│ Groups: vault-admins, vault-readonly
▼
User Identity
Why Authentik? I already had it running for GitLab, Grafana, and other services. Adding Vault to the same identity provider gives:
- ✅ Single source of truth for identities
- ✅ Centralized MFA enforcement (TOTP, WebAuthn)
- ✅ Group-based RBAC (manage access in one place)
- ✅ Audit trail (who accessed what, when, from where)
- ✅ Token expiration (force re-authentication every 8 hours)
Implementation with Terraform
1. Enable OIDC Auth Method
# terraform/modules/vault/auth-methods.tf
resource "vault_jwt_auth_backend" "authentik" {
path = "oidc"
type = "oidc"
oidc_discovery_url = "https://auth.mittbachweg.de/application/o/vault/"
oidc_client_id = var.authentik_client_id
oidc_client_secret = var.authentik_client_secret
default_role = "default"
tune {
default_lease_ttl = "8h"
max_lease_ttl = "24h"
}
}
Key parameters:
oidc_discovery_url: Authentik’s OIDC discovery endpointdefault_lease_ttl: Tokens expire after 8 hours (force re-authentication)
2. Configure OIDC Roles
resource "vault_jwt_auth_backend_role" "default" {
backend = vault_jwt_auth_backend.authentik.path
role_name = "default"
bound_audiences = [var.authentik_client_id]
user_claim = "sub"
role_type = "oidc"
token_policies = [
"default",
"read-own-token"
]
groups_claim = "groups"
allowed_redirect_uris = [
"http://localhost:8250/oidc/callback",
"https://vault.one.mittbachweg.de/ui/vault/auth/oidc/oidc/callback"
]
claim_mappings = {
preferred_username = "username"
email = "email"
}
}
Role breakdown:
bound_audiences: Only accept tokens for this Vault instance (prevents token replay)groups_claim: Extract Authentik group memberships from JWTallowed_redirect_uris: Whitelist for OAuth2 redirect (CLI and Web UI)
3. Group-Based Policies
Map Authentik groups to Vault policies:
# Admin group gets full access
resource "vault_identity_group" "vault_admins" {
name = "vault-admins"
type = "external"
policies = [
"admin",
"create-tokens",
"manage-auth"
]
}
# Readonly group gets limited access
resource "vault_identity_group" "vault_readonly" {
name = "vault-readonly"
type = "external"
policies = [
"read-secrets",
"read-own-token"
]
}
# Bind to Authentik groups
resource "vault_identity_group_alias" "vault_admins" {
name = "vault-admins"
mount_accessor = vault_jwt_auth_backend.authentik.accessor
canonical_id = vault_identity_group.vault_admins.id
}
Policy example (admin.hcl):
# Full access to secrets
path "secret/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
# Manage auth methods
path "auth/*" {
capabilities = ["create", "read", "update", "delete", "list", "sudo"]
}
Authentik Configuration
Create an OAuth2/OpenID provider in Authentik:
# OAuth2 Provider for Vault
resource "authentik_provider_oauth2" "vault" {
name = "Vault OIDC Provider"
client_id = "vault"
client_secret = random_password.vault_oauth_secret.result
authorization_flow = data.authentik_flow.default-provider-authorization-implicit-consent.id
invalidation_flow = data.authentik_flow.default-invalidation-flow.id
# Redirect URIs for Vault UI, API, and CLI
allowed_redirect_uris = [
{
matching_mode = "strict"
url = "https://vault.one.mittbachweg.de/ui/vault/auth/oidc/oidc/callback"
},
{
matching_mode = "strict"
url = "https://vault.one.mittbachweg.de/oidc/callback"
},
{
matching_mode = "strict"
url = "http://localhost:8250/oidc/callback"
},
]
# Property mappings for OAuth scopes
property_mappings = [
data.authentik_property_mapping_provider_scope.scope-email.id,
data.authentik_property_mapping_provider_scope.scope-profile.id,
data.authentik_property_mapping_provider_scope.scope-openid.id,
]
# Additional configuration for proper OIDC flow
signing_key = data.authentik_certificate_key_pair.default.id
}
# Vault OIDC Application
resource "authentik_application" "vault" {
name = "HashiCorp Vault"
slug = "vault"
protocol_provider = authentik_provider_oauth2.vault.id
meta_description = "OIDC authentication for HashiCorp Vault"
meta_publisher = "Mittbachweg Infrastructure"
policy_engine_mode = "any"
}
Usage
CLI Login
$ vault login -method=oidc
# Browser opens for Authentik login
# After success:
Success! You are now authenticated.
$ vault token lookup
Key Value
--- -----
entity_id 8a7b9c1d-2e3f-4a5b-6c7d-8e9f0a1b2c3d
expire_time 2024-02-15T19:20:00Z
policies [default read-secrets]
Web UI Login
Navigate to https://vault.one.mittbachweg.de → Select “OIDC” method → Redirects to Authentik
Ansible Integration
# playbooks/vault_lookup.yml
- name: Fetch secret from Vault
set_fact:
db_password: "{{ lookup('community.hashi_vault.hashi_vault_read',
'secret=ansible/data/database',
auth_method='oidc',
url='https://vault.one.mittbachweg.de'
).password }}"
For automation, use service accounts with AppRole auth:
resource "vault_auth_backend" "approle" {
type = "approle"
}
resource "vault_approle_auth_backend_role" "ansible" {
backend = vault_auth_backend.approle.path
role_name = "ansible"
token_policies = ["read-secrets"]
token_ttl = 3600
}
Lessons Learned
What worked:
- ✅ OIDC login eliminated root token dependency
- ✅ Group-based RBAC simplified permission management
- ✅ MFA enforcement via Authentik blocked unauthorized access
- ✅ Token expiration forced regular re-authentication
What didn’t:
- ❌ Initial 8-hour token TTL was too short for long-running tasks (extended to 24h max)
- ❌ Forgot to configure service accounts for CI/CD (added AppRole auth)
- ❌ Debugging OIDC callback issues required checking Vault AND Authentik logs
The security improvement is worth the complexity. No more root tokens in plaintext files.