feat: add Keycloak SSO + oauth2-proxy + ArgoCD OIDC config
All checks were successful
AI Review / AI Code Review (pull_request) Successful in 1s
PR Checks / Validate & Security Scan (pull_request) Successful in 7s

- Keycloak (Bitnami Helm chart) with PostgreSQL on Longhorn
- oauth2-proxy for arch-docs dev/staging auth
- ArgoCD OIDC integration via ConfigMap
- Realm 'infrastructure': users admin/claude, groups infra-admins/infra-bots
- 4 OIDC clients: grafana, argocd, gitea, oauth2-proxy
- NetworkPolicy: default-deny + selective allow
- oauth2-proxy ingress for dev/staging subdomains
This commit is contained in:
root 2026-02-16 19:48:43 +01:00
parent b7d00da5f4
commit 2277d3592d
9 changed files with 570 additions and 0 deletions

View File

@ -0,0 +1,16 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-cm
namespace: argocd
labels:
app.kubernetes.io/name: argocd-cm
app.kubernetes.io/part-of: argocd
data:
url: "https://127.0.0.1:30443"
oidc.config: |
name: Keycloak
issuer: https://keycloak.georgepet.duckdns.org/realms/infrastructure
clientID: argocd
clientSecret: $oidc.keycloak.clientSecret
requestedScopes: ["openid", "profile", "email", "groups"]

View File

@ -0,0 +1,14 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-rbac-cm
namespace: argocd
labels:
app.kubernetes.io/name: argocd-rbac-cm
app.kubernetes.io/part-of: argocd
data:
policy.csv: |
g, /infra-admins, role:admin
g, /infra-bots, role:readonly
policy.default: "role:readonly"
scopes: "[groups]"

View File

@ -0,0 +1,205 @@
---
# Default deny all traffic
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: keycloak
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
---
# Allow DNS egress for all pods
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns-egress
namespace: keycloak
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- to:
- namespaceSelector: {}
ports:
- port: 53
protocol: UDP
- port: 53
protocol: TCP
---
# Keycloak: allow ingress from nginx-ingress
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-keycloak-from-nginx
namespace: keycloak
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: keycloak
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress-nginx
ports:
- port: 8080
protocol: TCP
---
# Keycloak → PostgreSQL
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-keycloak-to-postgres
namespace: keycloak
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: keycloak
policyTypes:
- Egress
egress:
- to:
- podSelector:
matchLabels:
app.kubernetes.io/name: postgresql
ports:
- port: 5432
protocol: TCP
---
# PostgreSQL: accept from Keycloak
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-postgres-from-keycloak
namespace: keycloak
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: postgresql
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app.kubernetes.io/name: keycloak
ports:
- port: 5432
protocol: TCP
---
# oauth2-proxy: allow ingress from nginx-ingress (auth subrequests)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-oauth2-proxy-from-nginx
namespace: keycloak
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: oauth2-proxy
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress-nginx
ports:
- port: 4180
protocol: TCP
---
# oauth2-proxy → Keycloak (internal + external OIDC)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-oauth2-proxy-egress
namespace: keycloak
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: oauth2-proxy
policyTypes:
- Egress
egress:
- to:
- podSelector:
matchLabels:
app.kubernetes.io/name: keycloak
ports:
- port: 8080
protocol: TCP
- to:
- ipBlock:
cidr: 0.0.0.0/0
ports:
- port: 443
protocol: TCP
- port: 80
protocol: TCP
---
# Keycloak config CLI job: egress to Keycloak (realm import)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-config-cli-to-keycloak
namespace: keycloak
spec:
podSelector:
matchLabels:
app.kubernetes.io/component: keycloak-config-cli
policyTypes:
- Egress
egress:
- to:
- podSelector:
matchLabels:
app.kubernetes.io/name: keycloak
ports:
- port: 8080
protocol: TCP
---
# cert-manager HTTP-01 solver
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-cert-manager-http01
namespace: keycloak
spec:
podSelector:
matchLabels:
acme.cert-manager.io/http01-solver: "true"
policyTypes:
- Ingress
- Egress
ingress:
- ports:
- port: 8089
protocol: TCP
egress:
- {}
---
# Keycloak: egress to internet for HTTPS (theme downloads, OIDC verification)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-keycloak-egress-https
namespace: keycloak
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: keycloak
policyTypes:
- Egress
egress:
- to:
- ipBlock:
cidr: 0.0.0.0/0
ports:
- port: 443
protocol: TCP

View File

@ -0,0 +1,52 @@
---
# oauth2-proxy ingress for dev subdomain
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: oauth2-proxy-dev
namespace: keycloak
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
ingressClassName: nginx
tls:
- hosts:
- dev.georgepet.duckdns.org
secretName: oauth2-proxy-dev-tls
rules:
- host: dev.georgepet.duckdns.org
http:
paths:
- path: /oauth2
pathType: Prefix
backend:
service:
name: oauth2-proxy
port:
number: 4180
---
# oauth2-proxy ingress for staging subdomain
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: oauth2-proxy-staging
namespace: keycloak
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
ingressClassName: nginx
tls:
- hosts:
- staging.georgepet.duckdns.org
secretName: oauth2-proxy-staging-tls
rules:
- host: staging.georgepet.duckdns.org
http:
paths:
- path: /oauth2
pathType: Prefix
backend:
service:
name: oauth2-proxy
port:
number: 4180

View File

@ -0,0 +1,121 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: keycloak-realm-config
namespace: keycloak
data:
realm-infrastructure.json: |
{
"realm": "infrastructure",
"enabled": true,
"sslRequired": "external",
"registrationAllowed": false,
"loginWithEmailAllowed": true,
"duplicateEmailsAllowed": false,
"resetPasswordAllowed": false,
"bruteForceProtected": true,
"permanentLockout": false,
"maxFailureWaitSeconds": 900,
"failureFactor": 5,
"users": [
{
"username": "admin",
"email": "admin@georgepet.duckdns.org",
"enabled": true,
"firstName": "Admin",
"groups": ["infra-admins"],
"credentials": [{"type": "password", "value": "changeme", "temporary": true}]
},
{
"username": "claude",
"email": "claude@georgepet.duckdns.org",
"enabled": true,
"firstName": "Claude",
"groups": ["infra-bots"],
"credentials": [{"type": "password", "value": "changeme", "temporary": true}]
}
],
"groups": [
{"name": "infra-admins"},
{"name": "infra-bots"}
],
"clients": [
{
"clientId": "grafana",
"name": "Grafana",
"enabled": true,
"clientAuthenticatorType": "client-secret",
"redirectUris": ["http://127.0.0.1:3001/*", "http://localhost:3001/*"],
"webOrigins": ["http://127.0.0.1:3001", "http://localhost:3001"],
"protocol": "openid-connect",
"publicClient": false,
"standardFlowEnabled": true,
"directAccessGrantsEnabled": false
},
{
"clientId": "argocd",
"name": "ArgoCD",
"enabled": true,
"clientAuthenticatorType": "client-secret",
"redirectUris": ["https://127.0.0.1:30443/*", "https://localhost:30443/*"],
"webOrigins": ["https://127.0.0.1:30443", "https://localhost:30443"],
"protocol": "openid-connect",
"publicClient": false,
"standardFlowEnabled": true,
"directAccessGrantsEnabled": false
},
{
"clientId": "gitea",
"name": "Gitea",
"enabled": true,
"clientAuthenticatorType": "client-secret",
"redirectUris": ["http://127.0.0.1:3000/*", "http://localhost:3000/*"],
"webOrigins": ["http://127.0.0.1:3000", "http://localhost:3000"],
"protocol": "openid-connect",
"publicClient": false,
"standardFlowEnabled": true,
"directAccessGrantsEnabled": false
},
{
"clientId": "oauth2-proxy",
"name": "OAuth2 Proxy",
"enabled": true,
"clientAuthenticatorType": "client-secret",
"redirectUris": [
"https://dev.georgepet.duckdns.org/oauth2/callback",
"https://staging.georgepet.duckdns.org/oauth2/callback"
],
"webOrigins": [
"https://dev.georgepet.duckdns.org",
"https://staging.georgepet.duckdns.org"
],
"protocol": "openid-connect",
"publicClient": false,
"standardFlowEnabled": true,
"directAccessGrantsEnabled": false
}
],
"clientScopes": [
{
"name": "groups",
"protocol": "openid-connect",
"attributes": {"include.in.token.scope": "true"},
"protocolMappers": [
{
"name": "groups",
"protocol": "openid-connect",
"protocolMapper": "oidc-group-membership-mapper",
"consentRequired": false,
"config": {
"full.path": "false",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "groups",
"userinfo.token.claim": "true"
}
}
]
}
],
"defaultDefaultClientScopes": ["openid", "profile", "email", "groups"]
}

View File

@ -0,0 +1,20 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: argocd-config
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: default
source:
repoURL: http://10.10.10.1:3000/claude/k8s-apps.git
targetRevision: main
path: apps/argocd-config
destination:
server: https://kubernetes.default.svc
namespace: argocd
syncPolicy:
automated:
prune: false
selfHeal: true

View File

@ -0,0 +1,22 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: keycloak-infra
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: default
source:
repoURL: http://10.10.10.1:3000/claude/k8s-apps.git
targetRevision: main
path: apps/keycloak
destination:
server: https://kubernetes.default.svc
namespace: keycloak
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=false

72
argocd-apps/keycloak.yaml Normal file
View File

@ -0,0 +1,72 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: keycloak
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: default
source:
chart: keycloak
repoURL: registry-1.docker.io/bitnamicharts
targetRevision: "25.2.0"
helm:
values: |
auth:
adminUser: admin
existingSecret: keycloak-secrets
passwordSecretKey: admin-password
production: true
proxy: edge
httpRelativePath: "/"
replicaCount: 1
resources:
requests:
cpu: 500m
memory: 1Gi
limits:
cpu: "1"
memory: 2Gi
keycloakConfigCli:
enabled: true
existingConfigmap: keycloak-realm-config
postgresql:
enabled: true
auth:
existingSecret: keycloak-secrets
secretKeys:
adminPasswordKey: password
userPasswordKey: password
primary:
resources:
requests:
cpu: 250m
memory: 512Mi
limits:
cpu: 500m
memory: 1Gi
persistence:
enabled: true
storageClass: longhorn
size: 8Gi
ingress:
enabled: true
ingressClassName: nginx
hostname: keycloak.georgepet.duckdns.org
tls: true
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
extraTls:
- hosts:
- keycloak.georgepet.duckdns.org
secretName: keycloak-tls
destination:
server: https://kubernetes.default.svc
namespace: keycloak
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=false

View File

@ -0,0 +1,48 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: oauth2-proxy
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: default
source:
chart: oauth2-proxy
repoURL: https://oauth2-proxy.github.io/manifests
targetRevision: "7.12.0"
helm:
values: |
replicaCount: 1
config:
clientID: "oauth2-proxy"
existingSecret: oauth2-proxy-secrets
configFile: |-
provider = "keycloak-oidc"
provider_display_name = "Keycloak"
oidc_issuer_url = "https://keycloak.georgepet.duckdns.org/realms/infrastructure"
email_domains = ["*"]
cookie_secure = true
cookie_domains = [".georgepet.duckdns.org"]
whitelist_domains = [".georgepet.duckdns.org"]
set_xauthrequest = true
set_authorization_header = true
pass_access_token = true
skip_provider_button = true
upstreams = ["static://202"]
cookie_samesite = "lax"
allowed_groups = ["/infra-admins"]
resources:
requests:
cpu: 25m
memory: 32Mi
limits:
cpu: 100m
memory: 128Mi
destination:
server: https://kubernetes.default.svc
namespace: keycloak
syncPolicy:
automated:
prune: true
selfHeal: true