Gate OIDC Signing-Key Rollovers with a Pinned JWKS
Block a risky OIDC signing-key rollover if the provider no longer publishes key IDs required by your clients.
A Kestra blueprint that lists Kubernetes ServiceAccounts with the plugin task, cross-references them against the pods using them, reports the unused ones on Slack, and optionally deletes them after a human approval gate.
get_accounts (io.kestra.plugin.kubernetes.kubectl.Get) lists the namespace's ServiceAccounts with the Kubernetes plugin task.list_pods (io.kestra.plugin.scripts.shell.Commands) dumps the namespace's pods as JSON with kubectl. Pods stay on kubectl because the plugin Get output carries metadata and status only, and the cross-reference needs spec.serviceAccountName, which is not in there.find_unused (io.kestra.plugin.scripts.python.Script) builds the set of accounts pods actually run as (a pod with no explicit serviceAccountName runs as default) and flags every ServiceAccount outside that set. It compares against the KV state so only newly found unused accounts are reported.maybe_alert (io.kestra.plugin.core.flow.If) posts the Slack report only when there is someone new to name.save_state (io.kestra.plugin.core.kv.Set) writes back the current unused list. Deleted accounts drop out on their own, so a reappearing unused account reports again, and steady-state reruns stay quiet.maybe_cleanup (io.kestra.plugin.core.flow.If) runs only when delete_after_approval is on and there is something to clean. approve_cleanup (io.kestra.plugin.core.flow.Pause) halts the run until a cluster admin resumes it from the UI with approval; if nobody responds within 24 hours the run cancels and nothing is deleted. check_approval then deletes each reported account (kubectl delete serviceaccount, one per account) and posts a Slack confirmation naming what was deleted.delete_after_approval on, deletion happens only after an admin approves the exact list at the Pause gate.Anyone whose namespaces have accumulated ServiceAccounts over months of deploys and wants the cleanup list, plus an approval-gated way to act on it without handing a bot standing delete rights over identity objects. Pairs with RBACWatch: this one covers unused accounts, that one covers over-powerful bindings.
kubectl get sa never told you which accounts are dead, and deleting them by hand is the kind of cleanup that never happens. As a flow it audits on a schedule, keeps every run's findings where you can review them, only bothers you when a new unused account appears, and puts the delete decision in front of a human with the list attached. Kestra holds the cluster credentials as secrets out of the YAML.
view ClusterRole, and mint a token with kubectl create token.| Secret | What it is |
|---|---|
CLIENT_CRT |
base64-encoded client certificate from your kubeconfig |
CLIENT_KEY |
base64-encoded client key from your kubeconfig |
MASTERURL |
base64-encoded Kubernetes API server URL, reachable from inside the Kestra container |
K8S_TOKEN |
base64-encoded ServiceAccount token for the plugin Get task |
SLACK_WEBHOOK |
base64-encoded Slack Incoming Webhook URL |
In Kestra OSS, secrets come from environment variables prefixed with SECRET_ and must be base64-encoded. If the API URL uses 127.0.0.1 or localhost, replace it with host.docker.internal so the Kestra container can reach it. trustCerts is on for the plugin task because minikube serves a self-signed certificate; on a cluster with a proper CA, drop it.
SECRET_ environment variables. If minikube restarted since your last run, regenerate SECRET_MASTERURL first: the API port changes on every minikube start. Mint the token: kubectl create serviceaccount kestra-reader -n default, kubectl create clusterrolebinding kestra-reader --clusterrole=view --serviceaccount=default:kestra-reader, then kubectl create token kestra-reader -n default and base64-encode it as SECRET_K8S_TOKEN.kubectl create sa orphan-sa (nothing uses it) and kubectl create sa used-sa, plus a pod that runs as used-sa. Run the flow with defaults: the Slack report names only orphan-sa, and nothing is deleted.delete_after_approval: true: the report posts, then the run pauses. Resume it from the execution's Gantt tab with approval checked: orphan-sa is deleted and a Slack confirmation names it. kubectl delete sa orphan-sa and run again: quiet, it dropped out of state.pauseDuration; behavior: CANCEL keeps the no-response case safe by cancelling instead of deleting.| Input | Type | Default | What it does |
|---|---|---|---|
namespace |
STRING | default |
the namespace whose ServiceAccounts to audit |
delete_after_approval |
BOOL | false |
when true, pause for admin approval and then delete the reported unused ServiceAccounts. when false, report only |
slack_webhook |
STRING | (required) | Incoming webhook URL of the Slack channel for the report |
delete_after_approval off, this flow is strictly read-only and never touches an account, whatever the report says.behavior: CANCEL cancels the run. Nothing is deleted on timeout, by design.default ServiceAccount is flagged when no pod runs as it. That is correct, and on a fresh namespace it may be the only finding.spec.serviceAccountName, and that field is not in the Get output.minikube start. Regenerate SECRET_MASTERURL and restart the Kestra container, or the steps fail to connect.