Kubernetes Vault PKI Access Design¶
Date: 2025-12-26 Status: Approved Goal: Enable identity-based kubectl access via Vault-issued short-lived client certificates, authenticated through Authentik SSO.
Overview¶
Use Vault PKI to issue short-lived client certificates for kubectl access. Users authenticate to Vault via OIDC (Authentik SSO), then request certificates based on their group membership. Existing certificate-based access remains for break-glass scenarios and automation.
Architecture¶
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ User │────▶│ Vault │────▶│ Authentik │────▶│ Vault │
│ (terminal) │ │ OIDC Auth │ │ (SSO) │ │ PKI Issue │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
│
Client Cert
│
▼
┌─────────────┐
│ k3s API │
│ (validates) │
└─────────────┘
Authentication Flow¶
- User runs
vault login -method=oidc→ browser opens to Authentik - Authentik authenticates, returns token to Vault with group claims
- Vault maps groups to policies (e.g.,
k8s-admins→ can issue admin certs) - User runs
k8s-login.sh admin→ Vault issues short-lived client cert - Helper script updates kubeconfig with new cert
- kubectl uses cert → k3s validates against cluster CA
Key Advantage¶
Zero changes to k3s API server. Certificate authentication already works.
Components¶
| Component | Location | Purpose |
|---|---|---|
| Authentik groups | tf/authentik |
k8s-admins, k8s-developers, k8s-viewers |
| Vault PKI roles | tf/vault |
Issue certs with fixed Organization field |
| Vault policies | tf/vault |
Control which roles each group can access |
| K8s RBAC | tf/vault |
Map cert Organization to K8s permissions |
| Helper script | scripts/ |
Automate cert request + kubeconfig update |
Vault PKI Configuration¶
PKI Mount Path¶
fzymgc-house/v1/ica1/v1/
PKI Roles¶
One role per access tier, with fixed Organization field:
resource "vault_pki_secret_backend_role" "k8s_admin" {
backend = "fzymgc-house/v1/ica1/v1"
name = "k8s-admin"
ttl = "8h"
max_ttl = "24h"
allowed_domains = ["fzymgc.house"]
allow_subdomains = false
allow_any_name = false
enforce_hostnames = false
key_usage = ["DigitalSignature", "KeyEncipherment"]
ext_key_usage = ["ClientAuth"]
organization = ["k8s-admins"] # Fixed - user cannot override
}
resource "vault_pki_secret_backend_role" "k8s_developer" {
# ... same config ...
name = "k8s-developer"
organization = ["k8s-developers"]
}
resource "vault_pki_secret_backend_role" "k8s_viewer" {
# ... same config ...
name = "k8s-viewer"
organization = ["k8s-viewers"]
}
Certificate to Kubernetes Identity Mapping¶
| Cert Field | K8s Mapping | Example |
|---|---|---|
CN (Common Name) |
Username | alice@fzymgc.house |
O (Organization) |
Groups | k8s-admins |
Multiple groups supported via multiple O= entries in cert.
Vault Policies¶
Policy Structure¶
# Policy: k8s-admin-cert (for k8s-admins group)
path "fzymgc-house/v1/ica1/v1/issue/k8s-admin" {
capabilities = ["create", "update"]
}
path "fzymgc-house/v1/ica1/v1/issue/k8s-developer" {
capabilities = ["create", "update"]
}
path "fzymgc-house/v1/ica1/v1/issue/k8s-viewer" {
capabilities = ["create", "update"]
}
# Policy: k8s-dev-cert (for k8s-developers group)
path "fzymgc-house/v1/ica1/v1/issue/k8s-developer" {
capabilities = ["create", "update"]
}
path "fzymgc-house/v1/ica1/v1/issue/k8s-viewer" {
capabilities = ["create", "update"]
}
# Policy: k8s-viewer-cert (for k8s-viewers group)
path "fzymgc-house/v1/ica1/v1/issue/k8s-viewer" {
capabilities = ["create", "update"]
}
Authentik Group → Vault Policy Mapping¶
| Authentik Group | Vault Policy | Can Issue |
|---|---|---|
k8s-admins |
k8s-admin-cert |
admin, developer, viewer certs |
k8s-developers |
k8s-dev-cert |
developer, viewer certs |
k8s-viewers |
k8s-viewer-cert |
viewer certs only |
Kubernetes RBAC¶
ClusterRoleBindings¶
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: vault-cert-cluster-admins
subjects:
- kind: Group
name: k8s-admins
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: cluster-admin
apiGroup: rbac.authorization.k8s.io
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: vault-cert-developers
subjects:
- kind: Group
name: k8s-developers
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: edit
apiGroup: rbac.authorization.k8s.io
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: vault-cert-viewers
subjects:
- kind: Group
name: k8s-viewers
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: view
apiGroup: rbac.authorization.k8s.io
Role Mapping Summary¶
| Cert Organization | K8s Group | ClusterRole | Access |
|---|---|---|---|
k8s-admins |
k8s-admins |
cluster-admin |
Full access |
k8s-developers |
k8s-developers |
edit |
Read/write workloads |
k8s-viewers |
k8s-viewers |
view |
Read-only |
Managed in: tf/vault/k8s-rbac-vault-certs.tf
Authentication Hierarchy¶
All methods work simultaneously:
| Method | Use Case | Token Lifetime |
|---|---|---|
| Static client certificates | Break-glass, automation, Windmill | Long-lived (1 year) |
| Vault-issued certificates | Daily human access | Short-lived (8 hours) |
| Service accounts | In-cluster pods | Configurable |
Break-Glass Protocol¶
The existing fzymgc-house-admin.yml kubeconfig remains valid. Store securely in:
1. Vault at secret/fzymgc-house/cluster/break-glass/kubeconfig
2. Offline backup (encrypted USB, password manager)
Helper Script¶
Location: scripts/k8s-login.sh
#!/usr/bin/env bash
set -euo pipefail
ROLE="${1:-viewer}"
CLUSTER="fzymgc-house"
API_SERVER="https://192.168.20.140:6443"
VAULT_PKI_PATH="fzymgc-house/v1/ica1/v1"
TTL="${K8S_CERT_TTL:-8h}"
KUBECONFIG_PATH="${KUBECONFIG:-$HOME/.kube/config}"
# Validate role
case "$ROLE" in
admin|developer|viewer) ;;
*) echo "Usage: k8s-login [admin|developer|viewer]"; exit 1 ;;
esac
# Get username from current Vault token
USERNAME=$(vault token lookup -format=json | jq -r '.data.display_name // .data.entity_id')
echo "Requesting $ROLE certificate for $USERNAME (TTL: $TTL)..."
# Issue certificate from Vault
CERT_DATA=$(vault write -format=json "$VAULT_PKI_PATH/issue/k8s-$ROLE" \
common_name="$USERNAME" \
ttl="$TTL")
# Extract cert and key
CLIENT_CERT=$(echo "$CERT_DATA" | jq -r '.data.certificate')
CLIENT_KEY=$(echo "$CERT_DATA" | jq -r '.data.private_key')
CA_CHAIN=$(echo "$CERT_DATA" | jq -r '.data.ca_chain[0]')
# Update kubeconfig
kubectl config set-cluster "$CLUSTER" \
--server="$API_SERVER" \
--certificate-authority=<(echo "$CA_CHAIN") \
--embed-certs=true
kubectl config set-credentials "$CLUSTER-$ROLE" \
--client-certificate=<(echo "$CLIENT_CERT") \
--client-key=<(echo "$CLIENT_KEY") \
--embed-certs=true
kubectl config set-context "$CLUSTER-$ROLE" \
--cluster="$CLUSTER" \
--user="$CLUSTER-$ROLE"
kubectl config use-context "$CLUSTER-$ROLE"
echo "✓ Configured context: $CLUSTER-$ROLE (expires in $TTL)"
User Experience¶
$ vault login -method=oidc
# Browser opens → Authentik SSO → success
$ k8s-login admin
Requesting admin certificate for alice@fzymgc.house (TTL: 8h)...
✓ Configured context: fzymgc-house-admin (expires in 8h)
$ kubectl get nodes
NAME STATUS ROLES AGE
tpi-alpha-1 Ready control-plane,master 204d
...
Implementation Plan¶
Deployment Order¶
| Phase | Component | Location | Notes |
|---|---|---|---|
| 1 | Authentik groups | tf/authentik |
Create k8s-admins, k8s-developers, k8s-viewers |
| 2 | PKI roles | tf/vault |
k8s-admin, k8s-developer, k8s-viewer roles |
| 3 | Vault policies | tf/vault |
Control who can issue which certs |
| 4 | OIDC group mapping | tf/vault |
Map Authentik groups to Vault policies |
| 5 | K8s RBAC | tf/vault |
ClusterRoleBindings for cert groups |
| 6 | Helper script | scripts/ |
k8s-login.sh for user convenience |
| 7 | Documentation | docs/ |
User guide for k8s access |
File Changes¶
tf/authentik/
└── kubernetes-groups.tf # New: k8s-admins, k8s-developers, k8s-viewers
tf/vault/
├── pki-k8s-roles.tf # New: PKI roles for k8s client certs
├── policy-k8s-access.tf # New: Policies for cert issuance
├── k8s-rbac-vault-certs.tf # New: ClusterRoleBindings
└── groups-and-roles.tf # Modify: Add OIDC group → policy mappings
scripts/
└── k8s-login.sh # New: Helper script
docs/
└── kubernetes-access.md # New: User guide
Testing Plan¶
- Create test user in each Authentik group
vault login -method=oidcas each user- Verify correct PKI role access (admin can issue admin, dev cannot)
- Run
k8s-login.shand verify kubectl works - Confirm RBAC limits access appropriately