diff --git a/.forgejo/scripts/deploy-prd.sh b/.forgejo/scripts/deploy-prd.sh index 8002b39..efc8972 100644 --- a/.forgejo/scripts/deploy-prd.sh +++ b/.forgejo/scripts/deploy-prd.sh @@ -23,6 +23,10 @@ secret_value() { printf '%s' "$encoded" | base64 -d 2>/dev/null || true } kubectl get --raw=/readyz >/dev/null +if [[ "$RELEASE" == go-loose ]] && ! kubectl -n "$NAMESPACE" get secret go-loose-wildcard-tls >/dev/null 2>&1; then + echo "Required go-loose-wildcard-tls Secret is missing; provision the tenant wildcard certificate before bootstrap/deploy." >&2 + exit 1 +fi kubectl create namespace "$NAMESPACE" --dry-run=client -o yaml | kubectl apply -f - for attempt in {1..360}; do if kubectl -n "$NAMESPACE" create configmap "$LOCK_NAME" --from-literal=owner="${GITHUB_REPOSITORY:-local}:${GITHUB_RUN_ID:-$$}" >/dev/null 2>&1; then LOCK_ACQUIRED=true; break; fi; sleep 5; done if [[ "$LOCK_ACQUIRED" != true ]]; then echo "Timed out waiting for another go-insane-prd deploy to release its lock." >&2; exit 1; fi @@ -69,18 +73,24 @@ if [[ "$RELEASE" != go-guess ]]; then HELM_ARGS+=(--set-string "image.repository [[ ! -f "$CHART/values-prd.yaml" ]] || HELM_ARGS+=(--values "$CHART/values-prd.yaml") case "$RELEASE" in go-loose) - if ! kubectl -n "$NAMESPACE" get secret go-loose-runtime >/dev/null 2>&1; then - db_password="$(secret_value go-loose-postgresql postgres-password)" - if [[ -z "$db_password" ]] && kubectl -n "$NAMESPACE" get pvc -o name | grep -Eq 'go-loose.*postgresql|data-go-loose'; then echo "Existing go-loose database storage has no recoverable password secret; refusing to rotate its credentials." >&2; exit 1; fi - [[ -n "$db_password" ]] || db_password="$(openssl rand -hex 24)" - session_secret="$(secret_value go-loose-env GO_LOOSE_SESSION_SECRET)" - [[ -n "$session_secret" ]] || session_secret="$(openssl rand -hex 32)" - kubectl -n "$NAMESPACE" create secret generic go-loose-runtime --from-literal=postgresql-password="$db_password" --from-literal=session-secret="$session_secret" - fi db_password="$(secret_value go-loose-runtime postgresql-password)" + [[ -n "$db_password" ]] || db_password="$(secret_value go-loose-postgresql postgres-password)" + [[ -n "$db_password" ]] || db_password="$(secret_value go-loose-postgresql password)" + if [[ -z "$db_password" ]] && kubectl -n "$NAMESPACE" get pvc -o name | grep -Eq 'go-loose.*postgresql|data-go-loose'; then + echo "Existing go-loose database storage has no recoverable password secret; refusing to rotate its credentials." >&2 + exit 1 + fi + [[ -n "$db_password" ]] || db_password="$(openssl rand -hex 24)" session_secret="$(secret_value go-loose-runtime session-secret)" + [[ -n "$session_secret" ]] || session_secret="$(secret_value go-loose-env GO_LOOSE_SESSION_SECRET)" + [[ -n "$session_secret" ]] || session_secret="$(openssl rand -hex 32)" database_url="postgres://goloose:${db_password}@go-loose-postgresql:5432/goloose?sslmode=disable" - HELM_ARGS+=(--set-string "postgresql.auth.password=${db_password}" --set-string "envSecrets.GO_LOOSE_DATABASE_URL=${database_url}" --set-string "envSecrets.GO_LOOSE_SESSION_SECRET=${session_secret}") + kubectl -n "$NAMESPACE" create secret generic go-loose-runtime \ + --from-literal=postgresql-password="$db_password" \ + --from-literal=postgres-password="$db_password" \ + --from-literal=database-url="$database_url" \ + --from-literal=session-secret="$session_secret" \ + --dry-run=client -o yaml | kubectl apply -f - ;; go-mad) if ! kubectl -n "$NAMESPACE" get secret go-mad-runtime >/dev/null 2>&1; then diff --git a/.forgejo/workflows/ci-cd.yaml b/.forgejo/workflows/ci-cd.yaml index bafb178..b8bb112 100644 --- a/.forgejo/workflows/ci-cd.yaml +++ b/.forgejo/workflows/ci-cd.yaml @@ -109,5 +109,4 @@ jobs: NEXUS_PASSWORD: ${{ secrets.NEXUS_PASSWORD }} run: | set -Eeuo pipefail - bash .forgejo/scripts/deploy-prd.sh deploy/helm/go-loose go-loose go-loose "${{ github.sha }}" \ - --set-string ingress.className=nginx + bash .forgejo/scripts/deploy-prd.sh deploy/helm/go-loose go-loose go-loose "${{ github.sha }}" diff --git a/deploy/helm/go-loose/Chart.yaml b/deploy/helm/go-loose/Chart.yaml index 8423780..0c18efa 100644 --- a/deploy/helm/go-loose/Chart.yaml +++ b/deploy/helm/go-loose/Chart.yaml @@ -2,7 +2,7 @@ apiVersion: v2 name: go-loose description: Authentication and authorization service for urpi platform. Provides OIDC, session management, and tenant isolation. type: application -version: 1.0.0 +version: 1.1.0 appVersion: "1.0.0" kubeVersion: ">=1.28.0-0" dependencies: diff --git a/deploy/helm/go-loose/README.md b/deploy/helm/go-loose/README.md new file mode 100644 index 0000000..cf1b2c6 --- /dev/null +++ b/deploy/helm/go-loose/README.md @@ -0,0 +1,81 @@ +# Go Loose Helm chart + +This is the canonical Go Loose chart. The central deploy script and the Go Loose +CI workflow both use this directory. + +## Environment profiles + +Use one profile file per environment: + +| Environment | Namespace | Profile | Ingress class | TLS issuer | +|---|---|---|---|---| +| `dev` | `go-insane-dev` | `values-dev.yaml` | nginx | `letsencrypt-staging` | +| `tst` | `go-insane-tst` | `values-tst.yaml` | nginx | `letsencrypt-staging` | +| `prd` | `go-insane-prd` | `values-prd.yaml` | nginx | `letsencrypt-prod` | +| `lcl` | `go-insane-lcl` | `values-lcl.yaml` | Traefik | local TLS | + +The namespace is supplied by Helm's release namespace; it is not hard-coded in +the chart templates. + +## Runtime Secret contract + +Before a release, the target namespace must contain `go-loose-runtime` with: + +| Key | Used for | +|---|---| +| `postgresql-password` | PostgreSQL application user's password | +| `postgres-password` | PostgreSQL administrator password | +| `database-url` | `GO_LOOSE_DATABASE_URL` | +| `session-secret` | `GO_LOOSE_SESSION_SECRET` | + +The chart reads these values using Kubernetes `secretKeyRef` and the PostgreSQL +subchart's `existingSecret` support. Runtime credentials are not stored in chart +values or Helm release configuration. + +To provision/update the secret from protected files without printing its values: + +```bash +/home/dnoulet/go/ops/scripts/apply-go-loose-secret.sh \ + \ + /secure/path/postgresql-password \ + /secure/path/database-url \ + /secure/path/session-secret \ + [kubeconfig] +``` + +The input files should contain only the required value (no trailing newline). +The helper creates the environment namespace if it is not present, then applies +the Secret. It does not print secret values. + +The PRD CI deployment script maintains the same secret contract automatically. +Do not commit the input files or put their contents in a Helm values file. + +## Tenant wildcard ingress + +Go Guess signs in through tenant-specific hosts such as +`nmbs.auth.dev-urpi.be` and `nmbs.auth.urpi.be`. The chart therefore +creates a second ingress for `*.auth-...` and requires a separate TLS Secret +named `go-loose-wildcard-tls` in the same namespace. The central deploy script +and PRD CI check for this Secret before installing the release. + +Provision it from a trusted wildcard certificate and private key: + +```bash +kubectl --kubeconfig --namespace go-insane- create secret tls \ + go-loose-wildcard-tls --cert=/secure/path/wildcard.crt \ + --key=/secure/path/wildcard.key +``` + +The PRD `letsencrypt-prod` issuer documented for the apex ingress uses HTTP-01; +HTTP-01 cannot issue wildcard certificates. Obtain this certificate through +DNS-01 or the approved certificate authority, then provision the Secret. Do not +put the private key in the chart, repository, or Helm release values. + +## Validation + +```bash +helm lint . -f values-dev.yaml +helm lint . -f values-tst.yaml +helm lint . -f values-prd.yaml +helm lint . -f values-lcl.yaml +``` diff --git a/deploy/helm/go-loose/templates/deployment.yaml b/deploy/helm/go-loose/templates/deployment.yaml index cf51f60..bd9fb28 100644 --- a/deploy/helm/go-loose/templates/deployment.yaml +++ b/deploy/helm/go-loose/templates/deployment.yaml @@ -40,14 +40,20 @@ spec: containerPort: {{ .Values.service.targetPort }} protocol: TCP env: + - name: GO_LOOSE_DATABASE_URL + valueFrom: + secretKeyRef: + name: {{ .Values.runtimeSecret.name }} + key: {{ .Values.runtimeSecret.keys.databaseURL }} + - name: GO_LOOSE_SESSION_SECRET + valueFrom: + secretKeyRef: + name: {{ .Values.runtimeSecret.name }} + key: {{ .Values.runtimeSecret.keys.sessionSecret }} {{- range $key, $value := .Values.env }} - name: {{ $key }} value: {{ $value | quote }} {{- end }} - {{- range $key, $value := .Values.envSecrets }} - - name: {{ $key }} - value: {{ $value | quote }} - {{- end }} livenessProbe: {{- toYaml .Values.livenessProbe | nindent 12 }} readinessProbe: @@ -65,4 +71,4 @@ spec: {{- with .Values.tolerations }} tolerations: {{- toYaml . | nindent 8 }} - {{- end }} \ No newline at end of file + {{- end }} diff --git a/deploy/helm/go-loose/templates/secret.yaml b/deploy/helm/go-loose/templates/secret.yaml deleted file mode 100644 index 5c0fab9..0000000 --- a/deploy/helm/go-loose/templates/secret.yaml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: v1 -kind: Secret -metadata: - name: {{ include "go-loose.fullname" . }}-env - namespace: {{ .Release.Namespace }} - labels: - {{- include "go-loose.labels" . | nindent 4 }} -type: Opaque -stringData: - {{- range $key, $value := .Values.envSecrets }} - {{ $key }}: {{ $value | quote }} - {{- end }} \ No newline at end of file diff --git a/deploy/helm/go-loose/templates/wildcard-ingress.yaml b/deploy/helm/go-loose/templates/wildcard-ingress.yaml new file mode 100644 index 0000000..b7fbfca --- /dev/null +++ b/deploy/helm/go-loose/templates/wildcard-ingress.yaml @@ -0,0 +1,30 @@ +{{- if .Values.wildcardIngress.enabled }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "go-loose.fullname" . }}-wildcard + namespace: {{ .Release.Namespace }} + labels: + {{- include "go-loose.labels" . | nindent 4 }} + {{- with .Values.wildcardIngress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + ingressClassName: {{ .Values.wildcardIngress.className }} + tls: + - secretName: {{ .Values.wildcardIngress.tlsSecretName }} + hosts: + - {{ .Values.wildcardIngress.host | quote }} + rules: + - host: {{ .Values.wildcardIngress.host | quote }} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: {{ include "go-loose.fullname" . }} + port: + number: {{ .Values.service.port }} +{{- end }} diff --git a/deploy/helm/go-loose/values-dev.yaml b/deploy/helm/go-loose/values-dev.yaml new file mode 100644 index 0000000..185b2f1 --- /dev/null +++ b/deploy/helm/go-loose/values-dev.yaml @@ -0,0 +1,46 @@ +replicaCount: 1 + +image: + repository: nexus.urpi.be/docker-hosted/go-loose + tag: latest + pullPolicy: Always + +imagePullSecrets: + - name: nexus-registry + +ingress: + enabled: true + className: nginx + annotations: + cert-manager.io/cluster-issuer: letsencrypt-staging + nginx.ingress.kubernetes.io/ssl-redirect: "true" + hosts: + - host: auth.dev-urpi.be + paths: + - path: / + pathType: Prefix + tls: + - secretName: go-loose-dev-tls + hosts: + - auth.dev-urpi.be + +wildcardIngress: + enabled: true + className: nginx + host: "*.auth.dev-urpi.be" + tlsSecretName: go-loose-wildcard-tls + annotations: + nginx.ingress.kubernetes.io/ssl-redirect: "true" + +postgresql: + primary: + persistence: + enabled: true + size: 5Gi + +env: + GO_LOOSE_BASE_URL: https://auth.dev-urpi.be + GO_LOOSE_AUTH_DOMAIN: auth.dev-urpi.be + GO_LOOSE_OIDC_REDIRECT_URL: https://auth.dev-urpi.be/auth/callback + GO_LOOSE_DEV_LOGIN: "true" + GO_LOOSE_LOCAL_LOGIN: "true" diff --git a/deploy/helm/go-loose/values-lcl.yaml b/deploy/helm/go-loose/values-lcl.yaml new file mode 100644 index 0000000..d17bfcc --- /dev/null +++ b/deploy/helm/go-loose/values-lcl.yaml @@ -0,0 +1,65 @@ +replicaCount: 1 +imagePullSecrets: [] + +ingress: + enabled: true + className: traefik + annotations: + cert-manager.io/cluster-issuer: urpi-private-ca + hosts: + # Apex only. The tenant hosts are served by wildcardIngress below, so that + # each host family keeps its own certificate: go-loose-lcl-tls covers the + # apex, go-loose-wildcard-tls covers *.auth.lcl-urpi.be. + - host: auth.lcl-urpi.be + paths: + - path: / + pathType: Prefix + tls: + - secretName: go-loose-lcl-tls + hosts: + - auth.lcl-urpi.be + +# Serves every .auth.lcl-urpi.be. Adding a tenant is then a row in the +# database only: no Helm upgrade, no new certificate, no per-host rule. +wildcardIngress: + enabled: true + className: traefik + host: "*.auth.lcl-urpi.be" + tlsSecretName: go-loose-wildcard-tls + annotations: + nginx.ingress.kubernetes.io/ssl-redirect: "true" + +certManager: + enabled: true + issuerRef: + name: urpi-private-ca + kind: ClusterIssuer + +postgresql: + enabled: true + image: + # Bitnami removed its versioned tags from Docker Hub; only :latest remains + # for bitnami/postgresql. The tag pinned in values.yaml (16.4.0) now 404s + # and leaves the StatefulSet in ImagePullBackOff. + tag: "latest" + auth: + database: goloose + username: goloose + password: "goloose" + existingSecret: go-loose-runtime + secretKeys: + adminPasswordKey: postgres-password + userPasswordKey: postgresql-password + primary: + persistence: + enabled: false + size: 5Gi + +env: + GO_LOOSE_ADDR: ":8080" + GO_LOOSE_BASE_URL: "https://lcl-urpi.be" + GO_LOOSE_AUTH_DOMAIN: "auth.lcl-urpi.be" + GO_LOOSE_OIDC_REDIRECT_URL: "https://lcl-urpi.be/auth/callback" + GO_LOOSE_DEV_LOGIN: "true" + GO_LOOSE_LOCAL_LOGIN: "true" + GO_LOOSE_ALLOW_PRIVATE_CONTRACT_URLS: "true" diff --git a/deploy/helm/go-loose/values-prd.yaml b/deploy/helm/go-loose/values-prd.yaml index 4ed5fda..93f4dcb 100644 --- a/deploy/helm/go-loose/values-prd.yaml +++ b/deploy/helm/go-loose/values-prd.yaml @@ -5,7 +5,7 @@ imagePullSecrets: - name: nexus-registry image: - repository: nexus-registry.urpi.be/docker-hosted/go-loose + repository: nexus.urpi.be/docker-hosted/go-loose tag: "0.0.1" pullPolicy: Always @@ -16,9 +16,10 @@ service: ingress: enabled: true - className: traefik + className: nginx annotations: cert-manager.io/cluster-issuer: letsencrypt-prod + nginx.ingress.kubernetes.io/ssl-redirect: "true" # nginx.ingress.kubernetes.io/ssl-redirect: "true" # nginx.ingress.kubernetes.io/proxy-body-size: "10m" hosts: @@ -31,6 +32,14 @@ ingress: hosts: - auth.urpi.be +wildcardIngress: + enabled: true + className: nginx + host: "*.auth.urpi.be" + tlsSecretName: go-loose-wildcard-tls + annotations: + nginx.ingress.kubernetes.io/ssl-redirect: "true" + certManager: enabled: true issuerRef: @@ -69,13 +78,9 @@ env: GO_LOOSE_AUTH_DOMAIN: "auth.urpi.be" GO_LOOSE_OIDC_REDIRECT_URL: "https://auth.urpi.be/auth/callback" GO_LOOSE_DEV_LOGIN: "false" - GO_LOOSE_LOCAL_LOGIN: "true" + GO_LOOSE_LOCAL_LOGIN: "false" GO_LOOSE_ALLOW_PRIVATE_CONTRACT_URLS: "true" -envSecrets: - GO_LOOSE_DATABASE_URL: "postgres://goloose:changeme@go-loose-postgresql:5432/goloose?sslmode=disable" - GO_LOOSE_SESSION_SECRET: "changeme-generate-secure-random" - livenessProbe: httpGet: path: /healthz @@ -92,4 +97,4 @@ readinessProbe: initialDelaySeconds: 10 periodSeconds: 10 timeoutSeconds: 3 - failureThreshold: 3 \ No newline at end of file + failureThreshold: 3 diff --git a/deploy/helm/go-loose/values-tst.yaml b/deploy/helm/go-loose/values-tst.yaml new file mode 100644 index 0000000..d656f2e --- /dev/null +++ b/deploy/helm/go-loose/values-tst.yaml @@ -0,0 +1,46 @@ +replicaCount: 1 + +image: + repository: nexus.urpi.be/docker-hosted/go-loose + tag: latest + pullPolicy: Always + +imagePullSecrets: + - name: nexus-registry + +ingress: + enabled: true + className: nginx + annotations: + cert-manager.io/cluster-issuer: letsencrypt-staging + nginx.ingress.kubernetes.io/ssl-redirect: "true" + hosts: + - host: auth.tst-urpi.be + paths: + - path: / + pathType: Prefix + tls: + - secretName: go-loose-tst-tls + hosts: + - auth.tst-urpi.be + +wildcardIngress: + enabled: true + className: nginx + host: "*.auth.tst-urpi.be" + tlsSecretName: go-loose-wildcard-tls + annotations: + nginx.ingress.kubernetes.io/ssl-redirect: "true" + +postgresql: + primary: + persistence: + enabled: true + size: 5Gi + +env: + GO_LOOSE_BASE_URL: https://auth.tst-urpi.be + GO_LOOSE_AUTH_DOMAIN: auth.tst-urpi.be + GO_LOOSE_OIDC_REDIRECT_URL: https://auth.tst-urpi.be/auth/callback + GO_LOOSE_DEV_LOGIN: "false" + GO_LOOSE_LOCAL_LOGIN: "false" diff --git a/deploy/helm/go-loose/values.yaml b/deploy/helm/go-loose/values.yaml index 5e49e67..2e2b0c1 100644 --- a/deploy/helm/go-loose/values.yaml +++ b/deploy/helm/go-loose/values.yaml @@ -54,6 +54,13 @@ ingress: hosts: - auth.urpi.be +wildcardIngress: + enabled: false + className: nginx + host: "*.auth.urpi.be" + tlsSecretName: go-loose-wildcard-tls + annotations: {} + certManager: enabled: true issuerRef: @@ -74,12 +81,24 @@ autoscaling: maxReplicas: 3 targetCPUUtilizationPercentage: 80 +runtimeSecret: + name: go-loose-runtime + keys: + databaseURL: database-url + sessionSecret: session-secret + postgresql: enabled: true + image: + tag: "16.4.0" auth: database: goloose username: goloose - password: changeme + password: "goloose" + existingSecret: go-loose-runtime + secretKeys: + adminPasswordKey: postgres-password + userPasswordKey: postgresql-password primary: persistence: enabled: true @@ -98,13 +117,9 @@ env: GO_LOOSE_AUTH_DOMAIN: "auth.urpi.be" GO_LOOSE_OIDC_REDIRECT_URL: "https://auth.urpi.be/auth/callback" GO_LOOSE_DEV_LOGIN: "false" - GO_LOOSE_LOCAL_LOGIN: "true" + GO_LOOSE_LOCAL_LOGIN: "false" GO_LOOSE_ALLOW_PRIVATE_CONTRACT_URLS: "true" -envSecrets: - GO_LOOSE_DATABASE_URL: "" - GO_LOOSE_SESSION_SECRET: "" - livenessProbe: httpGet: path: /healthz @@ -125,4 +140,4 @@ readinessProbe: nodeSelector: {} tolerations: [] -affinity: {} \ No newline at end of file +affinity: {} diff --git a/docs/commands b/docs/commands new file mode 100644 index 0000000..da94146 --- /dev/null +++ b/docs/commands @@ -0,0 +1,18 @@ +# Delete the incomplete secret +kubectl delete secret go-loose-runtime --kubeconfig=/home/dnoulet/.kube/config_lcl --insecure-skip-tls-verify -n go-insane-lcl --ignore-not-found + +# Recreate it with all required properties mapped correctly +kubectl create secret generic go-loose-runtime \ + --from-literal=postgres-password="goloose" \ + --from-literal=postgresql-password="goloose" \ + --from-literal=session-secret="localdevsecret123" \ + --from-literal=database-url="postgres://goloose:goloose@go-loose-postgresql:5432/goloose?sslmode=disable" \ + --kubeconfig=/home/dnoulet/.kube/config_lcl \ + --insecure-skip-tls-verify \ + -n go-insane-lcl + +KUBECONFIG=~/.kube/config_lcl kubectl config set-cluster local --insecure-skip-tls-verify=true + +openssl s_client -showcerts -connect rancher.intern-urpi.be:443 /dev/null | sed -n '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > ~/.kube/rancher-ca.crt +KUBECONFIG=~/.kube/config_lcl kubectl config set-cluster local --certificate-authority=/home/dnoulet/.kube/rancher-ca.crt +KUBECONFIG=~/.kube/config_lcl kubectl config set-cluster local --insecure-skip-tls-verify=false