The agent could only reach lab-mgmt, so it couldn't verify nodes, CNI or system pods on lab1. It correctly refused the admin ~/.kube/lab1.kubeconfig when the owner's permission was relayed through the main session. Workload kubeconfigs: - make-kubeconfig.sh: new --capi-cluster [NS/]NAME. --admin-kubeconfig is then the management cluster. The workload admin kubeconfig is read from Secret NS/NAME-kubeconfig into a mktemp file, removed on exit, so no admin file for the workload cluster lands on disk. - ansible/mgmt.yml --tags inspector now also mints kubeconfigs for every cluster in the new inspector_workload_clusters (group_vars: lab1). Clusters without a <name>-kubeconfig Secret are skipped. The tag is now the single weekly token refresh; tasks stay changed_when: false, so the playbook's changed=0 rule still holds. - Owner ran --tags inspector: kubeconfigs/lab1.kubeconfig (lab1-ro) minted, can-i checks as expected. Agent prompt: - Cluster map: what to inspect on lab-mgmt vs the workload cluster. Machine -> Node -> VM via nodeRef, providerID and ProxmoxMachine.spec.virtualMachineID (checked on live objects). - New triage step: scale-down blockers (non-DaemonSet kube-system pods without PDB, local storage, bare pods, PDBs at 0, safe-to-evict / scale-down-disabled). - Refuses admin kubeconfigs even when a caller relays permission; points the owner to --tags inspector on missing file or 401. - Verb before --kubeconfig so commands match the kubectl get/describe/logs allow rules; single-quote jsonpath/custom-columns for zsh. - Report format: abnormal first, then health summary, then unverified. settings.json: allow kubectl describe/logs/events and clusterctl describe. All four are read-only, and the RO kubeconfigs enforce read-only anyway. Checked: bash -n on the script; ansible-playbook --syntax-check and ansible-lint --profile production pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
81 lines
3.5 KiB
Bash
Executable File
81 lines
3.5 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Mint a read-only kubeconfig for the cluster-inspector agent.
|
|
#
|
|
# Usage: make-kubeconfig.sh <name> [options]
|
|
# <name> cluster name, e.g. lab-mgmt or lab1 -> kubeconfigs/<name>.kubeconfig
|
|
# --admin-kubeconfig PATH admin kubeconfig used to apply RBAC / mint the token (default: $KUBECONFIG or ~/.kube/config)
|
|
# --context CTX context inside the admin kubeconfig
|
|
# --capi-cluster [NS/]NAME workload cluster: --admin-kubeconfig is the management cluster; the workload
|
|
# admin kubeconfig is read from Secret NS/NAME-kubeconfig (NS default: default)
|
|
# into a temp file that is removed on exit
|
|
# --server URL override API server URL (k3s.yaml says https://127.0.0.1:6443)
|
|
# --duration DUR token lifetime (default 168h); re-run the script to refresh
|
|
# --apply-rbac kubectl apply rbac.yaml first (needed once per cluster)
|
|
set -euo pipefail
|
|
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
name=""; admin="${KUBECONFIG:-$HOME/.kube/config}"; ctx=""; capi=""; server=""; duration="168h"; apply=0
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--admin-kubeconfig) admin="$2"; shift 2 ;;
|
|
--context) ctx="$2"; shift 2 ;;
|
|
--capi-cluster) capi="$2"; shift 2 ;;
|
|
--server) server="$2"; shift 2 ;;
|
|
--duration) duration="$2"; shift 2 ;;
|
|
--apply-rbac) apply=1; shift ;;
|
|
-h|--help) sed -n '2,13p' "$0"; exit 0 ;;
|
|
-*) echo "unknown option $1" >&2; exit 1 ;;
|
|
*) name="$1"; shift ;;
|
|
esac
|
|
done
|
|
[[ -n "$name" ]] || { sed -n '2,13p' "$0" >&2; exit 1; }
|
|
|
|
k=(kubectl --kubeconfig "$admin")
|
|
[[ -n "$ctx" ]] && k+=(--context "$ctx")
|
|
|
|
if [[ -n "$capi" ]]; then
|
|
ns="default"; cluster="$capi"
|
|
[[ "$capi" == */* ]] && { ns="${capi%%/*}"; cluster="${capi#*/}"; }
|
|
workload="$(mktemp)" # mktemp creates it 0600
|
|
trap 'rm -f "$workload"' EXIT
|
|
"${k[@]}" -n "$ns" get secret "$cluster-kubeconfig" -o jsonpath='{.data.value}' | base64 -d > "$workload"
|
|
[[ -s "$workload" ]] || { echo "Secret $ns/$cluster-kubeconfig is empty or missing" >&2; exit 1; }
|
|
k=(kubectl --kubeconfig "$workload")
|
|
fi
|
|
|
|
if (( apply )); then "${k[@]}" apply -f "$here/rbac.yaml"; fi
|
|
|
|
token="$("${k[@]}" -n kube-system create token readonly-inspector --duration "$duration")"
|
|
[[ -n "$server" ]] || server="$("${k[@]}" config view --raw --minify --flatten -o jsonpath='{.clusters[0].cluster.server}')"
|
|
ca="$("${k[@]}" config view --raw --minify --flatten -o jsonpath='{.clusters[0].cluster.certificate-authority-data}')"
|
|
[[ -n "$ca" ]] || { echo "no certificate-authority-data in admin kubeconfig" >&2; exit 1; }
|
|
|
|
mkdir -p "$here/kubeconfigs"
|
|
out="$here/kubeconfigs/$name.kubeconfig"
|
|
umask 077
|
|
cat > "$out" <<EOF
|
|
apiVersion: v1
|
|
kind: Config
|
|
clusters:
|
|
- name: $name
|
|
cluster:
|
|
server: $server
|
|
certificate-authority-data: $ca
|
|
users:
|
|
- name: $name-readonly-inspector
|
|
user:
|
|
token: $token
|
|
contexts:
|
|
- name: $name-ro
|
|
context:
|
|
cluster: $name
|
|
user: $name-readonly-inspector
|
|
current-context: $name-ro
|
|
EOF
|
|
echo "wrote $out (context $name-ro, server $server, token valid $duration)"
|
|
|
|
echo "verification (expect yes, yes, no, no, no; CAPI rows say no on workload clusters):"
|
|
for check in "list pods" "get machines.cluster.x-k8s.io" "delete machines.cluster.x-k8s.io" \
|
|
"patch machinedeployments.cluster.x-k8s.io" "get secrets"; do
|
|
printf ' %-45s %s\n' "$check" "$(KUBECONFIG="$out" kubectl auth can-i $check -A 2>/dev/null || true)"
|
|
done
|