diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..db59ba9 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Force LF line endings for shell scripts +*.sh text eol=lf diff --git a/bootstrap.sh b/bootstrap.sh index 6c1170a..f653bbf 100644 --- a/bootstrap.sh +++ b/bootstrap.sh @@ -1,4 +1,5 @@ #!/bin/zsh + # in case of $'\r': command not found error, run command below first # sed -i 's/\r$//' ./bootstrap.sh @@ -27,8 +28,8 @@ Bootstrap() Gitea() { echo "Installing secret..." - kubectl apply -f private/gitea-repo-main.yaml - kubectl apply -f private/main.key + kubectl apply -f "private/${CLUSTER}/gitea-repo-main.yaml" + kubectl apply -f "private/${CLUSTER}/main.key" } ############################################################ @@ -36,10 +37,15 @@ Gitea() ############################################################ ArgoCd() { + # Pre-create ConfigMap for repo-server env (must exist before Helm upgrade) + kubectl create namespace argocd --dry-run=client -o yaml | kubectl apply -f - + kubectl apply -f cluster-resources/argocd-repo-server-config.yaml + # install argocd echo "Installing ArgoCD..." helm upgrade --install argocd argo-cd \ --repo https://argoproj.github.io/argo-helm \ + --version "7.8.0" \ --namespace argocd --create-namespace \ --values infra/values/base/argocd-values.yaml \ --values "infra/values/${CLUSTER}/argocd-values.yaml" \ @@ -49,4 +55,4 @@ ArgoCd() kubectl apply -f "_app-of-apps-${CLUSTER}.yaml" -n argocd } -# Bootstrap +Bootstrap diff --git a/cluster-resources/argocd-oidc-secret-sync.yaml b/cluster-resources/argocd-oidc-secret-sync.yaml new file mode 100644 index 0000000..b61da8d --- /dev/null +++ b/cluster-resources/argocd-oidc-secret-sync.yaml @@ -0,0 +1,83 @@ +# CronJob: syncs OIDC client secret from registrar-managed +# argocd-oidc-credentials into argocd-secret (oidc.clientSecret key). +# Runs every 2 min. No-ops if source secret doesn't exist yet +# (safe for fresh deploys before Keycloak is up). +apiVersion: v1 +kind: ServiceAccount +metadata: + name: argocd-oidc-sync + namespace: argocd +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: argocd-oidc-sync + namespace: argocd +rules: +- apiGroups: [""] + resources: ["secrets"] + resourceNames: ["argocd-oidc-credentials", "argocd-secret"] + verbs: ["get", "patch"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: argocd-oidc-sync + namespace: argocd +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: argocd-oidc-sync +subjects: +- kind: ServiceAccount + name: argocd-oidc-sync + namespace: argocd +--- +apiVersion: batch/v1 +kind: CronJob +metadata: + name: argocd-oidc-sync + namespace: argocd +spec: + schedule: "*/2 * * * *" + concurrencyPolicy: Forbid + successfulJobsHistoryLimit: 1 + failedJobsHistoryLimit: 3 + jobTemplate: + spec: + backoffLimit: 1 + template: + spec: + serviceAccountName: argocd-oidc-sync + restartPolicy: Never + containers: + - name: sync + image: bitnami/kubectl:latest + command: ["/bin/sh", "-c"] + args: + - | + set -e + + # Exit gracefully if source secret doesn't exist yet + if ! kubectl get secret argocd-oidc-credentials -n argocd >/dev/null 2>&1; then + echo "argocd-oidc-credentials not found — skipping (Keycloak not ready yet)" + exit 0 + fi + + # Read current OIDC client secret + NEW_SECRET=$(kubectl get secret argocd-oidc-credentials -n argocd \ + -o jsonpath='{.data.client-secret}' | base64 -d) + + # Read current value in argocd-secret (if any) + CURRENT=$(kubectl get secret argocd-secret -n argocd \ + -o jsonpath='{.data.oidc\.clientSecret}' 2>/dev/null | base64 -d || echo "") + + # Only patch if changed + if [ "$NEW_SECRET" = "$CURRENT" ]; then + echo "oidc.clientSecret already up to date" + exit 0 + fi + + kubectl patch secret argocd-secret -n argocd --type merge \ + -p "{\"stringData\":{\"oidc.clientSecret\":\"${NEW_SECRET}\"}}" + echo "Patched argocd-secret with oidc.clientSecret" diff --git a/cluster-resources/argocd-repo-server-config.yaml b/cluster-resources/argocd-repo-server-config.yaml new file mode 100644 index 0000000..6bef4b2 --- /dev/null +++ b/cluster-resources/argocd-repo-server-config.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: argocd-repo-server-config + namespace: argocd +data: + # Disable git submodule checkout - submodules (e.g. shared-prompts) + # are not needed for K8s manifest generation + ARGOCD_GIT_MODULES_ENABLED: "false" diff --git a/docs/REFERENCE.md b/docs/REFERENCE.md index d8d6d7a..a507d9c 100644 --- a/docs/REFERENCE.md +++ b/docs/REFERENCE.md @@ -654,8 +654,70 @@ retry: |---------|-------|---------| | `application.resourceTrackingMethod` | `annotation` | Track resources via annotations | | `timeout.reconciliation` | `60s` | Reconciliation interval | -| `admin.enabled` | `true` | Enable admin account | -| `git.submodule.enabled` | `false` | Disable git submodule checkout — submodules are not needed for manifest generation | +| `admin.enabled` | `false` | Admin login disabled (SSO-only) | +| `url` | `https://argocd.forteapps.net` | External URL for ArgoCD UI | + +**Git Submodule Disable**: Set via `configs.params` (NOT `repoServer.env` — that causes strategic merge conflicts with chart's `valueFrom` entries): +```yaml +configs: + params: + "reposerver.enable.git.submodule": "false" +``` +This writes to `argocd-cmd-params-cm` ConfigMap, which the chart already reads via `valueFrom`. Submodules (e.g., `shared-prompts`) are not needed for K8s manifest generation. + +**Break-Glass Admin Access**: Admin login is disabled (`admin.enabled: false`). The admin password remains in `argocd-secret`. To re-enable temporarily: +```bash +# Enable admin login +kubectl patch cm argocd-cm -n argocd -p '{"data":{"admin.enabled":"true"}}' +# Log in as admin, do what's needed, then disable again +kubectl patch cm argocd-cm -n argocd -p '{"data":{"admin.enabled":"false"}}' +``` +ArgoCD picks up ConfigMap changes within the reconciliation timeout (60s). Note: ArgoCD will revert this on next sync — this is intentional (temporary access only). + +**OIDC Authentication** (Keycloak): +```yaml +configs: + cm: + oidc.config: | + name: Forte SSO + issuer: https://id.forteapps.net/realms/forte + clientID: argocd + clientSecret: $oidc.clientSecret + requestedScopes: ["openid", "email", "profile"] + rbacConfig: + policy.csv: | + g, ArgoCD Admins, role:admin + g, ArgoCD Viewers, role:readonly + # Deny users not in any declared KC group + policy.default: "" + scopes: '[groups]' +``` + +**Access Control**: Only users in Keycloak groups `ArgoCD Admins` or `ArgoCD Viewers` can access ArgoCD. Users not in either group are denied (empty `policy.default`). Assign users to groups in Keycloak admin console. + +- ArgoCD does NOT add `openid` implicitly — must include in `requestedScopes` +- Do NOT add `groups` as a scope — the KC groups mapper emits the claim regardless +- `$oidc.clientSecret` references the `oidc.clientSecret` key in `argocd-secret` +- OIDC secret is synced by CronJob `argocd-oidc-sync` (see `cluster-resources/argocd-oidc-secret-sync.yaml`) +- The CronJob bridges `argocd-oidc-credentials` (from KC registrar) → `argocd-secret` every 2 min +- Safe for fresh deploys: no-ops if source secret doesn't exist yet + +**Ingress** (Traefik + TLS): +```yaml +server: + ingress: + enabled: true + ingressClassName: traefik + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + tls: true + extraArgs: + - --insecure +configs: + params: + "server.insecure": true +``` +TLS terminates at Traefik; ArgoCD runs in insecure mode behind the proxy. --- @@ -776,6 +838,15 @@ kubeStateMetrics: - Loki - Tempo +**Ingress**: Exposed via Traefik at `https://grafana.forteapps.net` with cert-manager TLS. + +**OIDC Authentication** (Keycloak): +- Uses `grafana.ini.auth.generic_oauth` with KC `grafana` client +- Secret `grafana-oidc-credentials` synced by KC registrar, loaded via `envFromSecrets` +- SSO-only mode: `auth.disable_login_form: true` + `auth.generic_oauth.auto_login: true` +- Role mapping via JMESPath on `resource_access.grafana.roles` claim (requires KC client role mapper) +- Roles: KC client roles `Admin`/`Editor` map to Grafana roles; default is `Viewer` + ### Loki **Chart**: `grafana/loki-stack` diff --git a/infra/base/keycloak.yaml b/infra/base/keycloak.yaml index a3234d8..937a243 100644 --- a/infra/base/keycloak.yaml +++ b/infra/base/keycloak.yaml @@ -15,7 +15,7 @@ spec: project: default sources: - - repoURL: https://charts.bitnami.com/bitnami + - repoURL: registry-1.docker.io/bitnamicharts chart: keycloak targetRevision: "25.2.0" helm: @@ -47,3 +47,7 @@ spec: kind: CronJob jsonPointers: - /spec/jobTemplate/spec/template/spec/containers/0/args + - group: apps + kind: StatefulSet + jsonPointers: + - /spec/volumeClaimTemplates diff --git a/infra/values/base/argocd-values.yaml b/infra/values/base/argocd-values.yaml index 4bfa6df..662812b 100644 --- a/infra/values/base/argocd-values.yaml +++ b/infra/values/base/argocd-values.yaml @@ -2,25 +2,40 @@ configs: secret: createSecret: true argocdServerAdminPassword: "$2b$12$Tmb1jH7ADvwWoUoNPXXsfOf6JqEluqhq8mL06a8DGT2AP1GzbNsCm" + # oidc.clientSecret managed by argocd-oidc-sync CronJob + # (reads from argocd-oidc-credentials, patches argocd-secret) ssh: knownHosts: | [git.forteapps.net]:2222 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDTwi40de8yTGUuRT0i/XGicQ672BLhYR6D/lDquJrp/tdrWoZhVVPy0wxSkWsq1V92iiAUuQnXagOGsLBGZT9uDLWKvEmNDnCfjzTMq3J1iA3vk2rQ8WBlCzhvmeCV/r0ufl6vsgfwxSRomLZeqa2UkLHx69gy2Njb1S2/aZK1Q53f466hCUfDULZrTn2Nn5Sj8cEbJ8EyvVN2YG9HYBxQdzKRPZEmS1vyzmn8YrYIkZseIRQElabzWGh86owuaaqnwJhTJj1j2sEUeIet04sGKJcnxx2UL4H90N66LKMldmMiuli+ve/CjJmMwDl0zGkjIniT3XR8CyEXYHli7B1hR8Z+dbK6DBgjz+28lFgMIRY70KkZJNsJcBNZLZ5fHwCI13a9U3Uhg3Pu/6s0zlosM4CrAQNQCRe95ZPtCpdFhlGrOl4m1rdSK2meL6rND0TBBuZbaFF6Py7TawLCAiO2KRaVqhu9OFVjwJ/nifgLzFGwWj+WcYmpuR+DwozrF/Hl7QYsz1x4GO1SONY07KbIFkUCHOMAh0AELY5YE4eGI4mtG6SecdPaAdLREGZYK4IcyP5i1QW9g0wmfRSsV9jy+r0ivBxixxh4yJiNpkg6NXak40gQtGIme9EJ+DxrRLruNsfDILWcdSuH/wvuorv56NpQFGB0FzB6LXMloSYptQ== cm: application.resourceTrackingMethod: annotation timeout.reconciliation: 60s - admin.enabled: "true" + # Admin login disabled — SSO only. Break-glass: kubectl patch cm argocd-cm -n argocd -p '{"data":{"admin.enabled":"true"}}' + admin.enabled: "false" + url: https://argocd.forteapps.net + oidc.config: | + name: Forte SSO + issuer: https://id.forteapps.net/realms/forte + clientID: argocd + clientSecret: $oidc.clientSecret + requestedScopes: ["openid", "email", "profile"] + rbac: + policy.csv: | + g, ArgoCD Admins, role:admin + g, ArgoCD Viewers, role:readonly + # Deny users not in any declared KC group (ArgoCD Admins / ArgoCD Viewers) + policy.default: "" + scopes: '[groups]' params: "server.insecure": true -repoServer: - env: - # Disable git submodule checkout - submodules (e.g. shared-prompts) - # are not needed for K8s manifest generation - - name: ARGOCD_GIT_MODULES_ENABLED - value: "false" + "reposerver.enable.git.submodule": "false" server: ingress: - enabled: false - ingressClassName: nginx + enabled: true + ingressClassName: traefik + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + tls: true extraArgs: - --insecure diff --git a/infra/values/base/keycloak-values.yaml b/infra/values/base/keycloak-values.yaml index 6fad310..109d14d 100644 --- a/infra/values/base/keycloak-values.yaml +++ b/infra/values/base/keycloak-values.yaml @@ -132,6 +132,49 @@ keycloakConfigCli: } } ] + }, + { + "clientId": "argocd", + "name": "ArgoCD", + "enabled": true, + "protocol": "openid-connect", + "clientAuthenticatorType": "client-secret", + "standardFlowEnabled": true, + "directAccessGrantsEnabled": false, + "publicClient": false, + "redirectUris": ["https://argocd.forteapps.net/auth/callback"], + "webOrigins": ["https://argocd.forteapps.net"], + "attributes": { + "k8s.secret.sync": "true", + "k8s.secret.namespace": "argocd", + "k8s.secret.name": "argocd-oidc-credentials", + "k8s.secret.client-id-key": "client-id", + "k8s.secret.client-secret-key": "client-secret" + }, + "protocolMappers": [ + { + "name": "groups", + "protocol": "openid-connect", + "protocolMapper": "oidc-group-membership-mapper", + "config": { + "claim.name": "groups", + "full.path": "false", + "id.token.claim": "true", + "access.token.claim": "true", + "userinfo.token.claim": "true" + } + } + ] + } + ], + "groups": [ + { + "name": "ArgoCD Admins", + "path": "/ArgoCD Admins" + }, + { + "name": "ArgoCD Viewers", + "path": "/ArgoCD Viewers" } ] } diff --git a/infra/values/upc-dev/argocd-values.yaml b/infra/values/upc-dev/argocd-values.yaml index 6ed9cea..a394511 100644 --- a/infra/values/upc-dev/argocd-values.yaml +++ b/infra/values/upc-dev/argocd-values.yaml @@ -1,5 +1,5 @@ global: - domain: argocd.127.0.0.1.nip.io + domain: argocd.forteapps.net notifications: context: clusterName: "dev-fd-eu-no-svg1"