Skip to main content

Phase 62 β€” IAM Hardening (OIDC kubectl + RBAC + MFA)

Completed 2026-06-22. Replaced the shared static kubeconfig with OIDC-based kubectl access via Authentik, enforced MFA for all users, disabled default service account token auto-mounting cluster-wide, and wired human RBAC to Authentik groups.


What Was Hardened​

AreaBeforeAfter
kubectl authShared X.509 cert in kubeconfigOIDC via Authentik (per-user, short-lived)
Human RBACNo RBAC bindings for OIDCClusterRoleBindings per Authentik group
MFAOptionalForced TOTP enrollment on first login
Default SA tokensAuto-mounted in every podDisabled cluster-wide (35 namespaces)

1. k3s OIDC Flags​

Added to /etc/rancher/k3s/config.yaml on set-hog (control plane):

kube-apiserver-arg:
- "oidc-issuer-url=https://auth.10.0.0.200.nip.io/application/o/kubernetes/"
- "oidc-client-id=kubernetes"
- "oidc-username-claim=preferred_username"
- "oidc-username-prefix=oidc:"
- "oidc-groups-claim=groups"
- "oidc-groups-prefix=oidc:"

The oidc: prefix on username and group claims prevents collisions between OIDC identities and internal Kubernetes service accounts (e.g. oidc:kanmegnea vs kanmegnea).

Authentik application: kubernetes (OIDC provider, authorization code + device flow).


2. kubelogin β€” OIDC Plugin for kubectl​

The standard kubectl does not support the OIDC Device Authorization Grant flow interactively. kubectl-oidc_login (int128/kubelogin) adds this as a credential plugin.

:::warning Wrong package brew install kubelogin installs Azure's kubelogin (for AKS) β€” a completely different tool. Install the int128 version directly: :::

# Mac
curl -Lo ~/.local/bin/kubectl-oidc_login \
https://github.com/int128/kubelogin/releases/latest/download/kubelogin_darwin_arm64.zip
# (unzip first, then move the binary)
chmod +x ~/.local/bin/kubectl-oidc_login

# Controller
curl -Lo ~/.local/bin/kubectl-oidc_login \
https://github.com/int128/kubelogin/releases/latest/download/kubelogin_linux_amd64.zip
chmod +x ~/.local/bin/kubectl-oidc_login

kubectl discovers it automatically when a kubeconfig references exec: command: kubectl-oidc_login.


3. kubeconfig Contexts​

Two contexts exist in ~/.kube/config:

ContextAuthWhen to use
minicloud-oidcOIDC via Authentik + TOTPDaily use
minicloud-break-glassStatic X.509 certAuthentik is down / emergency only

minicloud-oidc kubeconfig excerpt​

users:
- name: oidc-user
user:
exec:
apiVersion: client.authentication.k8s.io/v1beta1
command: kubectl-oidc_login
args:
- get-token
- --oidc-issuer-url=https://auth.10.0.0.200.nip.io/application/o/kubernetes/
- --oidc-client-id=kubernetes
- --oidc-client-secret=<client-secret>
- --oidc-extra-scope=groups
- --grant-type=authcode-keyboard

--grant-type=authcode-keyboard uses the device authorization flow β€” opens a browser URL for Authentik login, no redirect URI needed.

Usage​

kubectl --context minicloud-oidc get nodes
# β†’ Opens browser β†’ Authentik login + TOTP β†’ token cached for 60 min

kubectl --context minicloud-break-glass get nodes
# β†’ Uses static cert β€” no browser, no MFA

4. ClusterRoleBindings (GitOps-managed)​

File: minicloud-gitops/manifests/rbac/00-oidc-clusterrolebindings.yaml

---
# Direction IT group β†’ cluster-admin
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: oidc-cluster-admin
subjects:
- kind: Group
name: "oidc:Direction IT"
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: cluster-admin
apiGroup: rbac.authorization.k8s.io
---
# CybersΓ©curitΓ© + Audit β†’ read-only
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: oidc-cluster-viewer
subjects:
- kind: Group
name: "oidc:CybersΓ©curitΓ©"
apiGroup: rbac.authorization.k8s.io
- kind: Group
name: "oidc:Audit"
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: view
apiGroup: rbac.authorization.k8s.io
---
# kanmegnea individual admin (break-glass-oidc)
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: oidc-cluster-admin-kanmegnea
subjects:
- kind: User
name: "oidc:kanmegnea"
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: cluster-admin
apiGroup: rbac.authorization.k8s.io

ArgoCD Application rbac (minicloud-gitops/apps/rbac.yaml) syncs this automatically.


5. RBAC Verification​

All personas tested via kubectl auth can-i --as impersonation β€” no interactive Authentik login needed:

# Direction IT β†’ cluster-admin
kubectl auth can-i get pods --all-namespaces \
--as=oidc:demo.it --as-group='oidc:Direction IT'
# β†’ yes

kubectl auth can-i delete pods --all-namespaces \
--as=oidc:demo.it --as-group='oidc:Direction IT'
# β†’ yes

# CybersΓ©curitΓ© β†’ view only
kubectl auth can-i get pods --all-namespaces \
--as=oidc:demo.cyber --as-group='oidc:CybersΓ©curitΓ©'
# β†’ yes

kubectl auth can-i delete pods --all-namespaces \
--as=oidc:demo.cyber --as-group='oidc:CybersΓ©curitΓ©'
# β†’ no

# No binding β†’ denied
kubectl auth can-i get pods --all-namespaces \
--as=oidc:demo.sinistres
# β†’ no
PersonaAuthentik groupget podsdelete pods
demo.itDirection ITyesyes
demo.cyberCybersΓ©curitΓ©yesno
demo.auditAudityesno
kanmegnea(individual binding)yesyes
demo.sinistres(no binding)nono

6. Authentik MFA Enforcement​

MFA was optional before Phase 62. All users are now forced to enroll TOTP on first login.

In the Authentik admin UI β†’ Flows β†’ default-authentication-flow β†’ add a MFA Validation stage:

FieldValue
Stagedefault-authentication-mfa-validation
not_configured_actionconfigure
configuration_stagesTOTP setup stage

not_configured_action: configure redirects users who have no MFA device to the TOTP setup flow on their next login. They cannot proceed until they enroll.


7. Disable Default SA Token Auto-Mount​

By default, Kubernetes automatically mounts a service account token into every pod. Most pods don't need it, and mounted tokens have been exploited in container breakout scenarios.

Patched all 35 namespaces:

for ns in $(kubectl get ns -o jsonpath='{.items[*].metadata.name}'); do
kubectl patch serviceaccount default -n $ns \
-p '{"automountServiceAccountToken": false}'
done

Rule for future namespaces: all new namespace manifests must include this patch on the default SA:

# In 00-namespace.yaml for any new namespace
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: default
namespace: <new-namespace>
automountServiceAccountToken: false

Pods that genuinely need API server access must create a dedicated ServiceAccount with automountServiceAccountToken: true and minimal RBAC β€” not rely on the default SA.


Operational Notes​

# Test OIDC kubectl (opens browser)
kubectl --context minicloud-oidc get nodes

# Emergency access (no Authentik needed)
kubectl --context minicloud-break-glass get nodes

# Verify RBAC for any user without logging in
kubectl auth can-i <verb> <resource> \
--as=oidc:<username> --as-group='oidc:<group>'

# Check SA token auto-mount on all namespaces
kubectl get serviceaccount default -A \
-o jsonpath='{range .items[*]}{.metadata.namespace}: {.automountServiceAccountToken}{"\n"}{end}'
# All should show: false

# Verify OIDC flags loaded on apiserver
ssh set-hog "sudo journalctl -u k3s --since '10 minutes ago' --no-pager | grep 'Running kube-apiserver' | grep oidc"