diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml deleted file mode 100644 index fc72bd6..0000000 --- a/.gitea/workflows/deploy.yml +++ /dev/null @@ -1,74 +0,0 @@ -name: Validate and deploy Kubernetes Dashboard - -on: - push: - pull_request: - -env: - DASHBOARD_CHART_VERSION: 7.14.0 - HELM_VERSION: v4.2.2 - KUBECTL_VERSION: v1.36.3 - KUBERNETES_API: https://host.containers.internal:6443 - KUBERNETES_TLS_SERVER_NAME: 192.168.0.2 - NAMESPACE: kubernetes-dashboard - -jobs: - validate: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Install pinned Helm - run: | - curl -fsSL -o "${RUNNER_TEMP}/helm.tgz" "https://get.helm.sh/helm-${HELM_VERSION}-linux-amd64.tar.gz" - tar -xzf "${RUNNER_TEMP}/helm.tgz" -C "${RUNNER_TEMP}" --strip-components=1 linux-amd64/helm - echo "${RUNNER_TEMP}" >> "${GITHUB_PATH}" - - run: bash test.sh - - deploy: - if: gitea.ref == 'refs/heads/main' - needs: validate - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Install pinned tools - run: | - curl -fsSL -o "${RUNNER_TEMP}/helm.tgz" "https://get.helm.sh/helm-${HELM_VERSION}-linux-amd64.tar.gz" - tar -xzf "${RUNNER_TEMP}/helm.tgz" -C "${RUNNER_TEMP}" --strip-components=1 linux-amd64/helm - curl -fsSL -o "${RUNNER_TEMP}/kubectl" "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl" - chmod 0700 "${RUNNER_TEMP}/helm" "${RUNNER_TEMP}/kubectl" - echo "${RUNNER_TEMP}" >> "${GITHUB_PATH}" - - name: Configure Kubernetes access - env: - KUBE_CONFIG_BASE64: ${{ secrets.KUBE_CONFIG_BASE64 }} - run: | - test -n "${KUBE_CONFIG_BASE64}" - printf '%s' "${KUBE_CONFIG_BASE64}" | base64 --decode > "${RUNNER_TEMP}/kubeconfig" - chmod 0600 "${RUNNER_TEMP}/kubeconfig" - export KUBECONFIG="${RUNNER_TEMP}/kubeconfig" - kubectl config set-cluster cluster --server="${KUBERNETES_API}" --tls-server-name="${KUBERNETES_TLS_SERVER_NAME}" - - name: Apply public desired state and verify - run: | - export KUBECONFIG="${RUNNER_TEMP}/kubeconfig" - ./render.sh "${RUNNER_TEMP}/dashboard.yaml" - kubectl apply --server-side --force-conflicts --dry-run=server --validate=false \ - --filename "${RUNNER_TEMP}/dashboard.yaml" \ - --filename auth-proxy.yaml \ - --filename kubernetes-dashboard-ingressroute.yaml - kubectl apply --server-side --force-conflicts --validate=false \ - --filename "${RUNNER_TEMP}/dashboard.yaml" \ - --filename auth-proxy.yaml \ - --filename kubernetes-dashboard-ingressroute.yaml - - bash ./wait-for-deployments.sh \ - kubernetes-dashboard-api \ - kubernetes-dashboard-auth \ - kubernetes-dashboard-web \ - kubernetes-dashboard-metrics-scraper \ - kubernetes-dashboard-kong \ - kubernetes-dashboard-auth-proxy - - status=$(curl --silent --show-error --output /dev/null --write-out '%{http_code}' --max-time 15 https://kubernetes.brunner.ninja/) - case "${status}" in - 302|303|307) ;; - *) echo "Expected Authentik redirect, received HTTP ${status}" >&2; exit 1 ;; - esac diff --git a/README.md b/README.md index 4633875..03c4f60 100644 --- a/README.md +++ b/README.md @@ -1,57 +1,36 @@ -# Kubernetes Dashboard on arschrock +# Kubernetes Dashboard chart -This repository pins the final official Kubernetes Dashboard chart, `7.14.0`, -directly from its GitHub release. The old Helm index -now returns 404 because Kubernetes Dashboard is archived and unmaintained. -Upstream recommends migrating to Headlamp; this deployment should therefore be -treated as an interim compatibility service, not a long-term platform choice. +This repository maintains a narrowly patched copy of the final upstream +Kubernetes Dashboard `7.14.0` chart, with the optional Authentik-facing +authentication proxy used by this homelab. Dashboard is +[archived upstream](https://github.com/kubernetes-retired/dashboard), so treat +this as a compatibility deployment rather than a source of application updates. -## Authentication and TLS +The chart keeps the upstream resource names and the Kong subchart. Its sole +upstream-template change adds `app.security.existingCsrfSecret`: when true, the +chart references the existing `kubernetes-dashboard-csrf` Secret but does not +render or rotate it. The Secret must contain `private.key` and is deliberately +not stored in the public GitOps repository. For a fresh, ordinary install, +leave this option false so the upstream chart creates the key. The optional +`authProxy.enabled` adds the private nginx proxy, projected service-account +token, Service, ConfigMap, NetworkPolicy, and PDB. The site-specific +cluster-admin binding and public Traefik route are not part of this chart. -The public route at `https://kubernetes.brunner.ninja` is protected by a -namespace-local Traefik forward-auth middleware that uses the shared Authentik -embedded outpost. Keeping the middleware local allows Traefik's safer -`allowCrossNamespace=false` setting to remain enabled. Authentik permits only -members of `authentik Admins`. A private nginx proxy then adds a projected token for the -existing `admin-user` service account. Kubernetes issues and rotates that token -automatically every hour; it is never stored in Git, a static Secret, Gitea, or -Traefik configuration. Supplying the Authorization header makes Dashboard skip -its bearer-token login page. +For an independent test installation: -Every Dashboard container retains a CPU request, right-sized to `25m` from an -observed idle use of roughly `1-2m`. Requests are not removed: they still give -the scheduler a placement signal while the higher CPU limits leave room for -interactive bursts. +```sh +./install.sh +``` -This preserves the previous cluster-admin behavior, but Kubernetes audit events -identify every Dashboard action as the same service account. Native Authentik -OIDC would provide per-user identity, but requires kube-apiserver configuration -and another OAuth proxy because this Dashboard has no native OIDC login. Prefer -Headlamp with native OIDC when replacing this retired application. +The homelab deployment is managed by Argo CD from +[homelab-gitops](https://code.brunner.ninja/feedc0de/homelab-gitops/src/branch/main/bootstrap/kubernetes-dashboard.yaml). +Do not run `install.sh` against that Argo-managed release. Its values and +cluster-admin binding are in `apps/kubernetes-dashboard/` in GitOps; the +existing CSRF Secret is a separately preserved cluster bootstrap object. +Argo skips the vendored Kong CRDs because this release does not use Kong's +Ingress Controller. -The Authentik application and provider are owned by the adjacent `authentik` -repository. Run its `install.sh` once before exposing this route; that installer -also attaches the provider to the embedded outpost. No Dashboard OAuth client -secret is required for forward authentication. - -Traefik terminates browser-facing HTTPS. Kong's internal HTTPS listener remains -enabled because the auth proxy sends a privileged bearer token to it. Kong's -plain HTTP listener is disabled. - -## Bootstrap and CI/CD - -Run `./install.sh` once with administrator access. It downloads and verifies the -pinned chart, repairs the existing failed Helm release, creates the bootstrap -RBAC, and waits for all six deployments. Run `./test.sh` for a secret-free chart -render and schema validation. - -Then run `./create-ci-kubeconfig.sh` and store its final line as the Gitea -Actions secret `KUBE_CONFIG_BASE64`. The CI identity can update only named, -already-existing Dashboard objects. It cannot read Secrets, edit the -`admin-user` cluster-admin binding, create resources, or delete resources. -Deployment readiness is checked by polling those exact object names; the CI -identity deliberately has no namespace-wide Deployment `list/watch` access. - -The deploy workflow runs only for `main`; pull requests and all pushes run the -validation job. The chart's generated CSRF Secret is created by the bootstrap -Helm installation and deliberately filtered out of the restricted CI render. +On main, Gitea Actions lints and packages the chart, publishes it to the +public indexed repository, and promotes its exact chart version in GitOps. +The Git tag `v7.14.0` follows the upstream Dashboard chart release; +chart-only commits append Git describe count and hash. diff --git a/auth-proxy.yaml b/auth-proxy.yaml deleted file mode 100644 index e149ce3..0000000 --- a/auth-proxy.yaml +++ /dev/null @@ -1,248 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: kubernetes-dashboard-auth-proxy - namespace: kubernetes-dashboard -data: - nginx.conf.template: | - pid /tmp/nginx.pid; - error_log /dev/stderr warn; - - events {} - - http { - access_log /dev/stdout; - client_body_temp_path /tmp/client-body; - proxy_temp_path /tmp/proxy; - fastcgi_temp_path /tmp/fastcgi; - uwsgi_temp_path /tmp/uwsgi; - scgi_temp_path /tmp/scgi; - - map $http_upgrade $connection_upgrade { - default upgrade; - '' close; - } - - server { - listen 8080; - server_name _; - - location = /healthz { - access_log off; - default_type text/plain; - return 200 'ok\n'; - } - - location / { - proxy_http_version 1.1; - proxy_pass https://kubernetes-dashboard-kong-proxy.kubernetes-dashboard.svc.cluster.local:443; - proxy_ssl_name kubernetes-dashboard-kong-proxy.kubernetes-dashboard.svc.cluster.local; - proxy_ssl_server_name on; - # Kong uses its chart-generated internal certificate. - proxy_ssl_verify off; - - proxy_set_header Authorization "Bearer @@DASHBOARD_TOKEN@@"; - proxy_set_header Host $http_host; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Host $http_host; - proxy_set_header X-Forwarded-Proto https; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - proxy_read_timeout 3600s; - proxy_send_timeout 3600s; - } - } - } - run.sh: | - #!/bin/sh - set -eu - - token_file=/var/run/secrets/dashboard/token - template=/config/nginx.conf.template - rendered=/tmp/nginx.conf - - render_config() { - dashboard_token=$(tr -d '\n' < "$token_file") - test -n "$dashboard_token" - sed "s|@@DASHBOARD_TOKEN@@|$dashboard_token|g" "$template" > "$rendered.new" - chmod 0600 "$rendered.new" - mv "$rendered.new" "$rendered" - unset dashboard_token - } - - stop() { - nginx -c "$rendered" -s quit 2>/dev/null || true - exit 0 - } - - trap stop INT TERM - render_config - token_digest=$(sha256sum "$token_file" | awk '{print $1}') - nginx -t -c "$rendered" - nginx -c "$rendered" - - while :; do - sleep 60 - next_digest=$(sha256sum "$token_file" | awk '{print $1}') - if [ "$next_digest" != "$token_digest" ]; then - render_config - nginx -t -c "$rendered" - nginx -c "$rendered" -s reload - token_digest=$next_digest - fi - done ---- -apiVersion: v1 -kind: Service -metadata: - name: kubernetes-dashboard-auth-proxy - namespace: kubernetes-dashboard -spec: - selector: - app.kubernetes.io/name: kubernetes-dashboard-auth-proxy - ports: - - name: http - port: 8080 - targetPort: http ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: kubernetes-dashboard-auth-proxy - namespace: kubernetes-dashboard -spec: - replicas: 2 - strategy: - type: RollingUpdate - rollingUpdate: - maxSurge: 1 - maxUnavailable: 0 - selector: - matchLabels: - app.kubernetes.io/name: kubernetes-dashboard-auth-proxy - template: - metadata: - labels: - app.kubernetes.io/name: kubernetes-dashboard-auth-proxy - app.kubernetes.io/part-of: kubernetes-dashboard - spec: - serviceAccountName: admin-user - automountServiceAccountToken: false - securityContext: - runAsNonRoot: true - runAsUser: 101 - runAsGroup: 101 - fsGroup: 101 - seccompProfile: - type: RuntimeDefault - containers: - - name: proxy - image: docker.io/nginxinc/nginx-unprivileged:1.30.6-alpine - imagePullPolicy: IfNotPresent - command: [/bin/sh, /config/run.sh] - ports: - - name: http - containerPort: 8080 - securityContext: - allowPrivilegeEscalation: false - readOnlyRootFilesystem: true - capabilities: - drop: [ALL] - resources: - requests: - cpu: 25m - memory: 32Mi - limits: - cpu: 200m - memory: 128Mi - readinessProbe: - httpGet: - path: /healthz - port: http - periodSeconds: 10 - livenessProbe: - httpGet: - path: /healthz - port: http - periodSeconds: 20 - volumeMounts: - - name: config - mountPath: /config - readOnly: true - - name: token - mountPath: /var/run/secrets/dashboard - readOnly: true - - name: temporary - mountPath: /tmp - volumes: - - name: config - configMap: - name: kubernetes-dashboard-auth-proxy - defaultMode: 0555 - - name: token - projected: - defaultMode: 0440 - sources: - - serviceAccountToken: - path: token - audience: https://kubernetes.default.svc.cluster.local - expirationSeconds: 3600 - - name: temporary - emptyDir: - sizeLimit: 64Mi ---- -apiVersion: policy/v1 -kind: PodDisruptionBudget -metadata: - name: kubernetes-dashboard-auth-proxy - namespace: kubernetes-dashboard -spec: - maxUnavailable: 1 - selector: - matchLabels: - app.kubernetes.io/name: kubernetes-dashboard-auth-proxy ---- -apiVersion: networking.k8s.io/v1 -kind: NetworkPolicy -metadata: - name: kubernetes-dashboard-auth-proxy - namespace: kubernetes-dashboard -spec: - podSelector: - matchLabels: - app.kubernetes.io/name: kubernetes-dashboard-auth-proxy - policyTypes: [Ingress, Egress] - ingress: - - from: - - namespaceSelector: - matchLabels: - kubernetes.io/metadata.name: default - podSelector: - matchLabels: - app.kubernetes.io/name: traefik - ports: - - protocol: TCP - port: 8080 - egress: - - to: - - namespaceSelector: - matchLabels: - kubernetes.io/metadata.name: kube-system - podSelector: - matchLabels: - k8s-app: kube-dns - ports: - - protocol: UDP - port: 53 - - protocol: TCP - port: 53 - - to: - - podSelector: - matchLabels: - app.kubernetes.io/instance: kubernetes-dashboard - app.kubernetes.io/name: kong - ports: - - protocol: TCP - port: 443 - - protocol: TCP - port: 8443 diff --git a/ci-deployer.yaml b/ci-deployer.yaml deleted file mode 100644 index 858eebc..0000000 --- a/ci-deployer.yaml +++ /dev/null @@ -1,130 +0,0 @@ -# Bootstrap once with an administrator identity. This credential can update -# only the existing Dashboard resources and cannot read Kubernetes Secrets. -apiVersion: v1 -kind: ServiceAccount -metadata: - name: kubernetes-dashboard-deployer - namespace: kubernetes-dashboard -automountServiceAccountToken: false ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - name: kubernetes-dashboard-deployer - namespace: kubernetes-dashboard -rules: - - apiGroups: [""] - resources: [serviceaccounts] - resourceNames: - - kubernetes-dashboard-kong - - kubernetes-dashboard-api - - kubernetes-dashboard-metrics-scraper - - kubernetes-dashboard-web - verbs: [get, patch, update] - - apiGroups: [""] - resources: [configmaps] - resourceNames: - - kong-dbless-config - - kubernetes-dashboard-web-settings - - kubernetes-dashboard-auth-proxy - verbs: [get, patch, update] - - apiGroups: [""] - resources: [services] - resourceNames: - - kubernetes-dashboard-kong-proxy - - kubernetes-dashboard-api - - kubernetes-dashboard-auth - - kubernetes-dashboard-metrics-scraper - - kubernetes-dashboard-web - - kubernetes-dashboard-auth-proxy - verbs: [get, patch, update] - - apiGroups: [apps] - resources: [deployments] - resourceNames: - - kubernetes-dashboard-kong - - kubernetes-dashboard-api - - kubernetes-dashboard-auth - - kubernetes-dashboard-metrics-scraper - - kubernetes-dashboard-web - - kubernetes-dashboard-auth-proxy - verbs: [get, patch, update] - - apiGroups: [policy] - resources: [poddisruptionbudgets] - resourceNames: - - kubernetes-dashboard - - kubernetes-dashboard-auth-proxy - verbs: [get, patch, update] - - apiGroups: [networking.k8s.io] - resources: [networkpolicies] - resourceNames: [kubernetes-dashboard-auth-proxy] - verbs: [get, patch, update] - - apiGroups: [rbac.authorization.k8s.io] - resources: [roles] - resourceNames: - - kubernetes-dashboard-api - - kubernetes-dashboard-web - verbs: [get, patch, update, escalate] - - apiGroups: [rbac.authorization.k8s.io] - resources: [rolebindings] - resourceNames: - - kubernetes-dashboard-api - - kubernetes-dashboard-web - verbs: [get, patch, update, bind] - - apiGroups: [traefik.io] - resources: [ingressroutes] - resourceNames: [kubernetes-dashboard-ingressroute] - verbs: [get, patch, update] - - apiGroups: [traefik.io] - resources: [middlewares] - resourceNames: [kubernetes-dashboard-authentik] - verbs: [get, patch, update] ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: kubernetes-dashboard-deployer - namespace: kubernetes-dashboard -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: Role - name: kubernetes-dashboard-deployer -subjects: - - kind: ServiceAccount - name: kubernetes-dashboard-deployer - namespace: kubernetes-dashboard ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: kubernetes-dashboard-deployer -rules: - - apiGroups: [rbac.authorization.k8s.io] - resources: [clusterroles] - resourceNames: [kubernetes-dashboard-metrics-scraper] - verbs: [get, patch, update, escalate] - - apiGroups: [rbac.authorization.k8s.io] - resources: [clusterrolebindings] - resourceNames: [kubernetes-dashboard-metrics-scraper] - verbs: [get, patch, update, bind] ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - name: kubernetes-dashboard-deployer -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: kubernetes-dashboard-deployer -subjects: - - kind: ServiceAccount - name: kubernetes-dashboard-deployer - namespace: kubernetes-dashboard ---- -apiVersion: v1 -kind: Secret -metadata: - name: kubernetes-dashboard-deployer-token - namespace: kubernetes-dashboard - annotations: - kubernetes.io/service-account.name: kubernetes-dashboard-deployer -type: kubernetes.io/service-account-token diff --git a/create-ci-kubeconfig.sh b/create-ci-kubeconfig.sh deleted file mode 100755 index 0f7a4df..0000000 --- a/create-ci-kubeconfig.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -project_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) -namespace=kubernetes-dashboard -account=kubernetes-dashboard-deployer -secret=kubernetes-dashboard-deployer-token -remote=${KUBECTL_SSH_HOST:-} - -cluster_kubectl() { - if [[ -n "${remote}" ]]; then - ssh "${remote}" kubectl "$@" - else - kubectl "$@" - fi -} - -if [[ -n "${remote}" ]]; then - cluster_kubectl apply --filename=- < "${project_dir}/ci-deployer.yaml" >&2 -else - cluster_kubectl apply --filename "${project_dir}/ci-deployer.yaml" >&2 -fi - -for attempt in {1..30}; do - data=$(cluster_kubectl --namespace "${namespace}" get secret "${secret}" \ - --output=jsonpath='{.data.token}' 2>/dev/null || true) - [[ -n "${data}" ]] && break - sleep 1 -done -[[ -n "${data:-}" ]] || { echo "Token was not issued" >&2; exit 1; } - -workdir=$(mktemp --directory) -trap 'rm -rf "${workdir}"' EXIT -server=$(cluster_kubectl config view --minify --output=jsonpath='{.clusters[0].cluster.server}') -cluster_kubectl --namespace "${namespace}" get secret "${secret}" \ - --output=jsonpath='{.data.ca\.crt}' | base64 --decode > "${workdir}/ca.crt" - -export KUBECONFIG="${workdir}/config" -kubectl config set-cluster cluster --server="${server}" \ - --certificate-authority="${workdir}/ca.crt" --embed-certs=true >/dev/null -kubectl config set-credentials "${account}" \ - --token="$(printf '%s' "${data}" | base64 --decode)" >/dev/null -kubectl config set-context kubernetes-dashboard --cluster=cluster \ - --user="${account}" --namespace="${namespace}" >/dev/null -kubectl config use-context kubernetes-dashboard >/dev/null - -echo "Store the following line as Gitea Actions secret KUBE_CONFIG_BASE64; do not commit it." >&2 -base64 --wrap=0 "${KUBECONFIG}" -printf '\n' diff --git a/filter-secrets.awk b/filter-secrets.awk deleted file mode 100644 index e67e05b..0000000 --- a/filter-secrets.awk +++ /dev/null @@ -1,23 +0,0 @@ -function emit_document() { - if (document != "" && !is_secret) { - printf "%s", document - } -} - -$0 == "---" { - emit_document() - document = "---\n" - is_secret = 0 - next -} - -{ - document = document $0 "\n" - if ($0 ~ /^kind:[[:space:]]*Secret[[:space:]]*$/) { - is_secret = 1 - } -} - -END { - emit_document() -} diff --git a/helm-plugins/pin-images/plugin.yaml b/helm-plugins/pin-images/plugin.yaml deleted file mode 100644 index de15b05..0000000 --- a/helm-plugins/pin-images/plugin.yaml +++ /dev/null @@ -1,8 +0,0 @@ -name: pin-images -version: 1.0.0 -type: postrenderer/v1 -apiVersion: v1 -runtime: subprocess -runtimeConfig: - platformCommand: - - command: "${HELM_PLUGIN_DIR}/../../pin-images.sh" diff --git a/install.sh b/install.sh index 150199e..cd05978 100755 --- a/install.sh +++ b/install.sh @@ -1,48 +1,6 @@ #!/bin/sh - set -eu - -chart_version=${DASHBOARD_CHART_VERSION:-7.14.0} project_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) -chart_archive=$(mktemp) -helm_cache=$(mktemp -d) -trap 'rm -f "$chart_archive"; rm -rf "$helm_cache"' EXIT HUP INT TERM - -for command in curl helm kubectl; do - if ! command -v "$command" >/dev/null 2>&1; then - echo "Required command not found: $command" >&2 - exit 1 - fi -done - -kubectl create namespace kubernetes-dashboard --dry-run=client --output=yaml \ - | kubectl apply --filename=- - -# This privileged identity is deliberately bootstrap-only. CI cannot edit the -# cluster-admin binding, and no long-lived service-account token is created. -kubectl apply --filename "$project_dir/kubernetes-dashboard-user.yaml" - -curl --fail --silent --show-error --location --output "$chart_archive" \ - "https://github.com/kubernetes-retired/dashboard/releases/download/kubernetes-dashboard-${chart_version}/kubernetes-dashboard-${chart_version}.tgz" - -HELM_CACHE_HOME="$helm_cache" HELM_PLUGINS="$project_dir/helm-plugins" \ -helm upgrade --install kubernetes-dashboard "$chart_archive" \ - --namespace kubernetes-dashboard \ - --values "$project_dir/kubernetes-dashboard-values.yaml" \ - --server-side=true --take-ownership --force-conflicts --rollback-on-failure \ +helm upgrade --install kubernetes-dashboard "$project_dir/chart" \ + --namespace kubernetes-dashboard --create-namespace \ --wait --timeout 15m - -kubectl apply --filename "$project_dir/auth-proxy.yaml" -kubectl apply --filename "$project_dir/kubernetes-dashboard-ingressroute.yaml" - -for deployment in \ - kubernetes-dashboard-api \ - kubernetes-dashboard-auth \ - kubernetes-dashboard-web \ - kubernetes-dashboard-metrics-scraper \ - kubernetes-dashboard-kong \ - kubernetes-dashboard-auth-proxy -do - kubectl --namespace kubernetes-dashboard rollout status \ - "deployment/$deployment" --timeout=10m -done diff --git a/kubernetes-dashboard-ingressroute.yaml b/kubernetes-dashboard-ingressroute.yaml deleted file mode 100644 index f33a0bb..0000000 --- a/kubernetes-dashboard-ingressroute.yaml +++ /dev/null @@ -1,40 +0,0 @@ -apiVersion: traefik.io/v1alpha1 -kind: Middleware -metadata: - name: kubernetes-dashboard-authentik - namespace: kubernetes-dashboard -spec: - forwardAuth: - address: http://authentik-server.authentik.svc.cluster.local/outpost.goauthentik.io/auth/traefik - authResponseHeaders: - - X-authentik-username - - X-authentik-groups - - X-authentik-entitlements - - X-authentik-email - - X-authentik-name - - X-authentik-uid - - X-authentik-jwt - - X-authentik-meta-jwks - - X-authentik-meta-outpost - - X-authentik-meta-provider - - X-authentik-meta-app - - X-authentik-meta-version - trustForwardHeader: true ---- -apiVersion: traefik.io/v1alpha1 -kind: IngressRoute -metadata: - name: kubernetes-dashboard-ingressroute - namespace: kubernetes-dashboard -spec: - entryPoints: - - websecure - routes: - - kind: Rule - match: Host(`kubernetes.brunner.ninja`) - middlewares: - - name: kubernetes-dashboard-authentik - services: - - kind: Service - name: kubernetes-dashboard-auth-proxy - port: http diff --git a/kubernetes-dashboard-user.yaml b/kubernetes-dashboard-user.yaml deleted file mode 100644 index 1711d0a..0000000 --- a/kubernetes-dashboard-user.yaml +++ /dev/null @@ -1,21 +0,0 @@ -# Bootstrap-only privileged identity. No static token Secret is created. -# The auth proxy receives an automatically rotated projected token. -apiVersion: v1 -kind: ServiceAccount -metadata: - name: admin-user - namespace: kubernetes-dashboard -automountServiceAccountToken: false ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - name: admin-user -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: cluster-admin -subjects: - - kind: ServiceAccount - name: admin-user - namespace: kubernetes-dashboard diff --git a/kubernetes-dashboard-values.yaml b/kubernetes-dashboard-values.yaml deleted file mode 100644 index 53599ff..0000000 --- a/kubernetes-dashboard-values.yaml +++ /dev/null @@ -1,106 +0,0 @@ -# Public, secret-free values for the final upstream Kubernetes Dashboard chart. -# pin-images.sh adds immutable linux/amd64 digests after Helm has rendered the -# version labels; the chart otherwise puts invalid digest strings in labels. -app: - image: - pullPolicy: IfNotPresent - security: - podDisruptionBudget: - enabled: true - maxUnavailable: 1 - settings: - global: - clusterName: arschrock - defaultNamespace: default - -auth: - image: - repository: docker.io/kubernetesui/dashboard-auth - tag: 1.4.0 - scaling: - replicas: 2 - revisionHistoryLimit: 3 - containers: - resources: - requests: - cpu: 25m - memory: 200Mi - limits: - cpu: 250m - memory: 400Mi - -api: - image: - repository: docker.io/kubernetesui/dashboard-api - tag: 1.14.0 - scaling: - replicas: 2 - revisionHistoryLimit: 3 - containers: - resources: - requests: - cpu: 25m - memory: 200Mi - limits: - cpu: 250m - memory: 400Mi - -web: - image: - repository: docker.io/kubernetesui/dashboard-web - tag: 1.7.0 - scaling: - replicas: 2 - revisionHistoryLimit: 3 - containers: - resources: - requests: - cpu: 25m - memory: 200Mi - limits: - cpu: 250m - memory: 400Mi - -metricsScraper: - enabled: true - image: - repository: docker.io/kubernetesui/dashboard-metrics-scraper - tag: 1.2.2 - scaling: - revisionHistoryLimit: 3 - containers: - resources: - requests: - cpu: 25m - memory: 200Mi - limits: - cpu: 250m - memory: 400Mi - -# Browser TLS terminates at Traefik. Kong still uses TLS because the private -# auth proxy sends the privileged projected bearer token on this hop. -kong: - enabled: true - replicaCount: 2 - resources: - requests: - cpu: 25m - memory: 256Mi - limits: - cpu: 500m - memory: 512Mi - proxy: - type: ClusterIP - http: - enabled: false - tls: - enabled: true - -cert-manager: - enabled: false - -nginx: - enabled: false - -metrics-server: - enabled: false diff --git a/render.sh b/render.sh deleted file mode 100755 index 1c79ad6..0000000 --- a/render.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -project_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) -version=${DASHBOARD_CHART_VERSION:-7.14.0} -output=${1:?Usage: render.sh OUTPUT_FILE} -chart=$(mktemp) -raw=$(mktemp) -helm_cache=$(mktemp --directory) -trap 'rm -f "${chart}" "${raw}"; rm -rf "${helm_cache}"' EXIT - -curl --fail --silent --show-error --location --output "${chart}" \ - "https://github.com/kubernetes-retired/dashboard/releases/download/kubernetes-dashboard-${version}/kubernetes-dashboard-${version}.tgz" - -HELM_CACHE_HOME="${helm_cache}" HELM_PLUGINS="${project_dir}/helm-plugins" \ -helm template kubernetes-dashboard "${chart}" \ - --namespace kubernetes-dashboard \ - --values "${project_dir}/kubernetes-dashboard-values.yaml" \ - --set-string app.security.csrfKey=ci-render-only-not-used > "${raw}" - -# The chart always renders a random CSRF Secret. It is created and retained by -# the bootstrap Helm installation; restricted CI never receives Secret access. -awk -f "${project_dir}/filter-secrets.awk" "${raw}" > "${output}" diff --git a/test.sh b/test.sh deleted file mode 100755 index 5b7d17e..0000000 --- a/test.sh +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -project_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) -workdir=$(mktemp --directory) -trap 'rm -rf "${workdir}"' EXIT - -sh -n "${project_dir}/install.sh" -bash -n \ - "${project_dir}/render.sh" \ - "${project_dir}/create-ci-kubeconfig.sh" \ - "${project_dir}/wait-for-deployments.sh" \ - "${project_dir}/test.sh" -awk -f "${project_dir}/filter-secrets.awk" /dev/null >/dev/null - -"${project_dir}/render.sh" "${workdir}/dashboard.yaml" - -if grep -Eq '^kind:[[:space:]]*Secret[[:space:]]*$' "${workdir}/dashboard.yaml"; then - echo "Restricted CI render contains a Secret" >&2 - exit 1 -fi -if rg -i 'Bearer [A-Za-z0-9._-]{20,}|token:[[:space:]]*[A-Za-z0-9._-]{20,}' "${project_dir}" \ - --glob '*.yaml' --glob '*.yml'; then - echo "A bearer token appears to be committed" >&2 - exit 1 -fi - -grep -Fq 'kubernetes-dashboard-auth-proxy' "${project_dir}/kubernetes-dashboard-ingressroute.yaml" -grep -Fq 'name: kubernetes-dashboard-authentik' "${project_dir}/kubernetes-dashboard-ingressroute.yaml" -grep -Fq 'authentik-server.authentik.svc.cluster.local/outpost.goauthentik.io/auth/traefik' \ - "${project_dir}/kubernetes-dashboard-ingressroute.yaml" -if grep -Fq 'namespace: default' "${project_dir}/kubernetes-dashboard-ingressroute.yaml"; then - echo "Dashboard route must not use a cross-namespace middleware" >&2 - exit 1 -fi -if grep -Fq 'rollout status' "${project_dir}/.gitea/workflows/deploy.yml"; then - echo "Restricted CI must poll exact Deployment names instead of list/watch" >&2 - exit 1 -fi -grep -Fq 'expirationSeconds: 3600' "${project_dir}/auth-proxy.yaml" -grep -Fq 'proxy_pass https://kubernetes-dashboard-kong-proxy' "${project_dir}/auth-proxy.yaml" -grep -Fq 'containerPort: 8443' "${workdir}/dashboard.yaml" -grep -Fq 'dashboard-api:1.14.0' "${workdir}/dashboard.yaml" - -request_count=$(awk ' - $1 == "cpu:" && $2 == "25m" { count++ } - END { print count + 0 } -' "${workdir}/dashboard.yaml") -if [[ "${request_count}" -lt 5 ]] || grep -Fq 'cpu: 100m' "${workdir}/dashboard.yaml"; then - echo "Expected every Dashboard chart component to request only 25m CPU" >&2 - exit 1 -fi - -image=ghcr.io/yannh/kubeconform:v0.7.0 -if command -v podman >/dev/null 2>&1; then - container_runtime=podman -elif command -v docker >/dev/null 2>&1; then - container_runtime=docker -else - echo "Podman or Docker is required for kubeconform validation" >&2 - exit 1 -fi -for file in \ - "${workdir}/dashboard.yaml" \ - "${project_dir}/kubernetes-dashboard-user.yaml" \ - "${project_dir}/auth-proxy.yaml" \ - "${project_dir}/kubernetes-dashboard-ingressroute.yaml" \ - "${project_dir}/ci-deployer.yaml" -do - "${container_runtime}" run --rm --interactive "${image}" \ - -strict -exit-on-error -ignore-missing-schemas -summary < "${file}" -done - -echo "Dashboard public desired state is valid and contains no static credentials." diff --git a/wait-for-deployments.sh b/wait-for-deployments.sh deleted file mode 100755 index 54c05fb..0000000 --- a/wait-for-deployments.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -namespace=${NAMESPACE:-kubernetes-dashboard} -timeout_seconds=${DEPLOYMENT_TIMEOUT_SECONDS:-600} -poll_seconds=${DEPLOYMENT_POLL_SECONDS:-5} - -if [[ "$#" -eq 0 ]]; then - echo "Usage: wait-for-deployments.sh DEPLOYMENT..." >&2 - exit 2 -fi - -for deployment in "$@"; do - deadline=$((SECONDS + timeout_seconds)) - state="not observed" - complete=false - - while ((SECONDS < deadline)); do - state=$(kubectl --namespace "${namespace}" get "deployment/${deployment}" \ - --output=jsonpath='{.metadata.generation}{"|"}{.status.observedGeneration}{"|"}{.spec.replicas}{"|"}{.status.updatedReplicas}{"|"}{.status.readyReplicas}{"|"}{.status.availableReplicas}{"\n"}') - IFS='|' read -r generation observed desired updated ready available <<< "${state}" - observed=${observed:-0} - desired=${desired:-0} - updated=${updated:-0} - ready=${ready:-0} - available=${available:-0} - - if [[ "${observed}" -ge "${generation}" \ - && "${updated}" -eq "${desired}" \ - && "${ready}" -eq "${desired}" \ - && "${available}" -eq "${desired}" ]]; then - echo "deployment/${deployment} is ready (${ready}/${desired})" - complete=true - break - fi - sleep "${poll_seconds}" - done - - if [[ "${complete}" != true ]]; then - echo "Timed out waiting for deployment/${deployment}; generation observed desired updated ready available: ${state}" >&2 - exit 1 - fi -done