Introduce CI/CD
This commit is contained in:
@@ -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
|
||||
@@ -0,0 +1,6 @@
|
||||
*.kubeconfig
|
||||
kubeconfig*
|
||||
rendered*.yaml
|
||||
*.tgz
|
||||
.env
|
||||
.DS_Store
|
||||
@@ -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
@@ -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
|
||||
@@ -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
|
||||
Executable
+50
@@ -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'
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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'
|
||||
@@ -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}"
|
||||
@@ -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."
|
||||
Reference in New Issue
Block a user