Introduce CI/CD
Validate and deploy Kubernetes Dashboard / validate (push) Successful in 17s
Validate and deploy Kubernetes Dashboard / deploy (push) Failing after 10m20s

This commit is contained in:
2026-09-05 17:56:41 +02:00
parent 273241a0b2
commit 1daf39f4fc
15 changed files with 869 additions and 1 deletions
+76
View File
@@ -0,0 +1,76 @@
name: Validate and deploy Kubernetes Dashboard
on:
push:
pull_request:
env:
DASHBOARD_CHART_VERSION: 7.14.0
DASHBOARD_CHART_SHA256: 37be462a39258fda57a2a5e5d81082d8d28a8083c7977f4f4632fa374b1a616d
HELM_VERSION: v4.2.2
HELM_SHA256: 9adafecab4d406853bba163a70e9f104f47dbbf65ce24b7653bae7e36150bcb6
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"
printf '%s %s\n' "${HELM_SHA256}" "${RUNNER_TEMP}/helm.tgz" | sha256sum --check
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"
printf '%s %s\n' "${HELM_SHA256}" "${RUNNER_TEMP}/helm.tgz" | sha256sum --check
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"
curl -fsSL -o "${RUNNER_TEMP}/kubectl.sha256" "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl.sha256"
printf '%s %s\n' "$(cat "${RUNNER_TEMP}/kubectl.sha256")" "${RUNNER_TEMP}/kubectl" | sha256sum --check
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
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 "${NAMESPACE}" rollout status "deployment/${deployment}" --timeout=10m
done
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
+6
View File
@@ -0,0 +1,6 @@
*.kubeconfig
kubeconfig*
rendered*.yaml
*.tgz
.env
.DS_Store
+49 -1
View File
@@ -1,2 +1,50 @@
# kubernetes-dashboard-deployment
# Kubernetes Dashboard on arschrock
This repository pins the final official Kubernetes Dashboard chart, `7.14.0`,
directly from its GitHub release with SHA-256 verification. 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.
## Authentication and TLS
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.
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 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.
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.
+248
View File
@@ -0,0 +1,248 @@
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.29.1-alpine@sha256:6742337773a7ca839e7a76f489cc684cc410e3de46c0c83be8bb2bb9944360ef
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
+130
View File
@@ -0,0 +1,130 @@
# 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
+50
View File
@@ -0,0 +1,50 @@
#!/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'
+23
View File
@@ -0,0 +1,23 @@
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()
}
+8
View File
@@ -0,0 +1,8 @@
name: pin-images
version: 1.0.0
type: postrenderer/v1
apiVersion: v1
runtime: subprocess
runtimeConfig:
platformCommand:
- command: "${HELM_PLUGIN_DIR}/../../pin-images.sh"
Executable
+51
View File
@@ -0,0 +1,51 @@
#!/bin/sh
set -eu
chart_version=${DASHBOARD_CHART_VERSION:-7.14.0}
chart_sha256=${DASHBOARD_CHART_SHA256:-37be462a39258fda57a2a5e5d81082d8d28a8083c7977f4f4632fa374b1a616d}
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 sha256sum; 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"
printf '%s %s\n' "$chart_sha256" "$chart_archive" | sha256sum --check >&2
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" \
--post-renderer pin-images \
--server-side=true --take-ownership --force-conflicts --rollback-on-failure \
--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
+40
View File
@@ -0,0 +1,40 @@
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
+21
View File
@@ -0,0 +1,21 @@
# 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
+77
View File
@@ -0,0 +1,77 @@
# 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
api:
image:
repository: docker.io/kubernetesui/dashboard-api
tag: 1.14.0
scaling:
replicas: 2
revisionHistoryLimit: 3
web:
image:
repository: docker.io/kubernetesui/dashboard-web
tag: 1.7.0
scaling:
replicas: 2
revisionHistoryLimit: 3
metricsScraper:
enabled: true
image:
repository: docker.io/kubernetesui/dashboard-metrics-scraper
tag: 1.2.2
scaling:
revisionHistoryLimit: 3
# 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
image:
unifiedRepoTag: docker.io/library/kong:3.9@sha256:8a639948bf940868c8b4bed32fccc53d27fdd5766dbc8702b65cacbec48dcfd0
effectiveSemver: 3.9.0
replicaCount: 2
resources:
requests:
cpu: 100m
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
Executable
+9
View File
@@ -0,0 +1,9 @@
#!/usr/bin/env bash
set -euo pipefail
sed \
-e 's|docker.io/kubernetesui/dashboard-api:1.14.0|docker.io/kubernetesui/dashboard-api:1.14.0@sha256:2bd14c0ffee99d15fb1595644ebd1083ac32c5157c6e6fd8615b0f556a1390c2|g' \
-e 's|docker.io/kubernetesui/dashboard-auth:1.4.0|docker.io/kubernetesui/dashboard-auth:1.4.0@sha256:484c65877a3aa9e7095745576e55310f09f135b2fe5d9694289352fe65986052|g' \
-e 's|docker.io/kubernetesui/dashboard-web:1.7.0|docker.io/kubernetesui/dashboard-web:1.7.0@sha256:5924e55a2eb65774b68e35fd0d0b41a3ef5dc6ef3e02dce6e340cf59b6d67d30|g' \
-e 's|docker.io/kubernetesui/dashboard-metrics-scraper:1.2.2|docker.io/kubernetesui/dashboard-metrics-scraper:1.2.2@sha256:0cdefa0492f2868b93f43880ffad4bd8ae8105c02dc5661352a0a40723b05dbc|g'
Executable
+27
View File
@@ -0,0 +1,27 @@
#!/usr/bin/env bash
set -euo pipefail
project_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
version=${DASHBOARD_CHART_VERSION:-7.14.0}
digest=${DASHBOARD_CHART_SHA256:-37be462a39258fda57a2a5e5d81082d8d28a8083c7977f4f4632fa374b1a616d}
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"
printf '%s %s\n' "${digest}" "${chart}" | sha256sum --check >&2
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 \
--post-renderer pin-images > "${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}"
Executable
+54
View File
@@ -0,0 +1,54 @@
#!/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}/pin-images.sh" \
"${project_dir}/render.sh" \
"${project_dir}/create-ci-kubeconfig.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
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@sha256:2bd14c0ffee99d15fb1595644ebd1083ac32c5157c6e6fd8615b0f556a1390c2' "${workdir}/dashboard.yaml"
image=ghcr.io/yannh/kubeconform:v0.7.0@sha256:85dbef6b4b312b99133decc9c6fc9495e9fc5f92293d4ff3b7e1b30f5611823c
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
docker 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."