Kubernetes Security & Hardening cheat sheet
102 commands from every lesson of Kubernetes Security & Hardening, on one page.
Inspect certificates
sudo kubeadm certs check-expiration | Expiry of every kubeadm-managed certificate |
openssl x509 -in cert.crt -noout -subject -issuer -enddate | Who, signed by whom, valid until |
openssl x509 -in apiserver.crt -noout -ext subjectAltName | Names and IPs the API server cert is valid for |
echo | openssl s_client -connect <host>:6443 2>/dev/null | openssl x509 -noout -dates | Check a live endpoint's certificate |
Kubelet certificates
ls -l /var/lib/kubelet/pki/ | kubelet client/serving certs on a node |
kubectl get csr | Certificate signing requests (e.g. kubelet serving certs) |
kubectl certificate approve <csr> | Approve a pending CSR (after checking it!) |
Install & inspect
helm repo add jetstack https://charts.jetstack.io | Add the cert-manager chart repository |
helm install cert-manager jetstack/cert-manager -n cert-manager --create-namespace --set crds.enabled=true | Install with CRDs (older charts: installCRDs=true) |
kubectl get clusterissuers,issuers -A | Configured issuers and their readiness |
kubectl get certificate -A | Certificates, Ready status and secret names |
kubectl describe certificate <name> | Events: why it isn't issued yet |
kubectl get certificaterequests,orders,challenges -A | The issuance pipeline (ACME) |
Who am I?
kubectl auth whoami | Username and groups the API server sees |
kubectl config view --minify | The user entry kubectl is using |
Client certificate via the CSR API
openssl genrsa -out asha.key 2048 | A private key |
openssl req -new -key asha.key -subj "/CN=asha/O=devops" -out asha.csr | CN = username, O = group |
kubectl certificate approve asha | Approve the CertificateSigningRequest |
kubectl get csr asha -o jsonpath='{.status.certificate}' | base64 -d > asha.crt | Fetch the signed certificate |
ServiceAccount tokens
kubectl create token app -n shop --duration=1h | A short-lived token for a ServiceAccount |
cat /var/run/secrets/kubernetes.io/serviceaccount/token | The projected token inside a pod |
API server (kube-apiserver flags)
--oidc-issuer-url=https://sso.example.com/realms/platform | Who issues the tokens (must be HTTPS) |
--oidc-client-id=kubernetes | The audience (client) tokens must be issued for |
--oidc-username-claim=email --oidc-username-prefix=oidc: | Which claim becomes the username (with a prefix) |
--oidc-groups-claim=groups --oidc-groups-prefix=oidc: | Which claim holds group membership |
kubectl side (kubelogin)
kubectl krew install oidc-login | Install kubelogin as a kubectl plugin |
kubectl oidc-login setup --oidc-issuer-url=… --oidc-client-id=kubernetes | Test the login and print the claims |
kubectl auth whoami | Confirm username and groups |
Design checklist
One source of identity | Enterprise IdP (Entra ID, Okta, Google…) for every cluster |
Broker when needed | Keycloak/Dex/Pinniped federate the IdP to many clusters |
MFA at the IdP | Kubernetes never sees passwords or second factors |
Short tokens | Minutes, refreshed by the IdP; faster offboarding |
Break-glass | Offline, sealed, monitored credentials: rotated after every use |
Useful commands
kubectl auth whoami | Confirm who you're logged in as |
sudo kubeadm kubeconfig user --client-name=breakglass-2026q3 --org=kubeadm:cluster-admins | Generate a separate emergency admin kubeconfig (kubeadm) |
Review access
kubectl auth can-i --list --as=oidc:asha@example.com --as-group=oidc:developers -n shop | Everything a user+group may do in a namespace |
kubectl get clusterrolebindings -o wide | Cluster-wide grants and their subjects |
kubectl who-can get secrets -n shop | Who can read secrets (kubectl-who-can plugin) |
kubectl get clusterroles -l rbac.authorization.k8s.io/aggregate-to-edit=true | Roles aggregated into 'edit' |
Dangerous verbs & resources
secrets: get/list/watch | Read every credential in scope |
pods: create (any) | Run as any ServiceAccount in that namespace, mount its Secrets |
bind / escalate on roles | Grant yourself more than you have |
impersonate | Act as another user or group |
nodes/proxy, pods/exec | Reach into nodes or containers |
Inspect
kubectl get ingressclass | Installed controllers (and the default) |
kubectl get ingress -A | Hosts, addresses and TLS per Ingress |
kubectl describe ingress <name> | Rules, backends and events |
Test TLS
curl -vk --resolve shop.example.com:443:<LB-IP> https://shop.example.com/ | Test a host before DNS points to it |
openssl s_client -connect <LB-IP>:443 -servername shop.example.com </dev/null | openssl x509 -noout -subject -dates | Which certificate is served for this name (SNI) |
Objects
GatewayClass | Which implementation (like IngressClass): owned by the infrastructure provider |
Gateway | Listeners (ports, protocols, hostnames, TLS): owned by the platform team |
HTTPRoute | Routing rules for an app: owned by the app team |
ReferenceGrant | Allows a reference into another namespace (e.g. a Route to a Service) |
Inspect
kubectl get gatewayclass,gateway -A | Implementations and gateways (with addresses) |
kubectl get httproute -A | Routes and their parent gateways |
kubectl describe httproute <name> | status.parents: Accepted / ResolvedRefs conditions |
Policy building blocks
podSelector: {} | All pods in the namespace |
policyTypes: ["Ingress", "Egress"] | Which directions this policy restricts |
namespaceSelector: {matchLabels: {kubernetes.io/metadata.name: shop}} | Pods in a specific namespace (label set automatically) |
ipBlock: {cidr: 10.0.0.0/8} | Traffic to/from IP ranges (outside the cluster) |
Test
kubectl -n shop exec deploy/web -- wget -qO- -T 3 http://api:8080 | Allowed? (timeout = blocked) |
kubectl get networkpolicy -A | Which namespaces are isolated |
Pod Security Admission (namespace labels)
kubectl label ns shop pod-security.kubernetes.io/enforce=restricted | Reject non-compliant pods |
kubectl label ns shop pod-security.kubernetes.io/warn=restricted | Warn clients but allow |
kubectl label ns shop pod-security.kubernetes.io/audit=restricted | Record violations in the audit log |
kubectl label --dry-run=server --overwrite ns shop pod-security.kubernetes.io/enforce=restricted | Preview which existing pods would violate |
Custom policy
kubectl get validatingadmissionpolicies | Built-in CEL policies |
kubectl get clusterpolicies | Kyverno policies |
kubectl get constrainttemplates,constraints | Gatekeeper policies |
Encryption at rest
--encryption-provider-config=/etc/kubernetes/enc/enc.yaml | kube-apiserver flag pointing at the EncryptionConfiguration |
etcdctl … get /registry/secrets/<ns>/<name> | hexdump -C | head | Check the raw value is encrypted (k8s:enc:… prefix) |
kubectl get secrets -A -o json | kubectl replace -f - | Rewrite all Secrets so they're re-encrypted with the current key |
Getting secrets into the cluster
kubectl get externalsecrets -A | External Secrets Operator: sync status |
kubeseal --format yaml < secret.yaml > sealed.yaml | Sealed Secrets: encrypt for Git |
vault kv get secret/shop/db | Read a secret from Vault (CLI) |
Inspect images
syft registry.example.com/shop/api:1.4.2 -o spdx-json > sbom.json | Generate an SBOM |
trivy image --severity HIGH,CRITICAL registry.example.com/shop/api:1.4.2 | Scan for known vulnerabilities |
trivy sbom sbom.json | Scan an existing SBOM |
crane digest registry.example.com/shop/api:1.4.2 | The immutable digest behind a tag |
Sign & verify (cosign)
cosign generate-key-pair | Create cosign.key / cosign.pub (or use keyless signing) |
cosign sign --key cosign.key <image>@sha256:<digest> | Sign an image by digest |
cosign verify --key cosign.pub <image>@sha256:<digest> | Verify the signature |
cosign attest --key cosign.key --type spdxjson --predicate sbom.json <image>@sha256:<digest> | Attach a signed SBOM attestation |
API audit (kube-apiserver flags)
--audit-policy-file=/etc/kubernetes/audit/policy.yaml | Which events to record, at which level |
--audit-log-path=/var/log/kubernetes/audit/audit.log | Where to write (or use a webhook backend) |
--audit-log-maxage=30 --audit-log-maxbackup=10 --audit-log-maxsize=200 | Rotation: days, files, MB |
jq 'select(.objectRef.resource=="secrets") | {user: .user.username, verb, ns: .objectRef.namespace}' audit.log | Who touched Secrets |
Falco
helm repo add falcosecurity https://falcosecurity.github.io/charts | Chart repository |
helm install falco falcosecurity/falco -n falco --create-namespace | Install Falco |
kubectl -n falco logs -l app.kubernetes.io/name=falco -f | Watch alerts |
Run kube-bench
kubectl apply -f https://raw.githubusercontent.com/aquasecurity/kube-bench/main/job.yaml | Run kube-bench as a Job on a node |
kubectl logs job/kube-bench | Read the report |
kube-bench run --targets node | Run only node checks (binary on a node) |
Kubelet settings to check (KubeletConfiguration)
authentication.anonymous.enabled: false | No anonymous kubelet API access |
authorization.mode: Webhook | Kubelet asks the API server who may do what |
readOnlyPort: 0 | Disable the unauthenticated read-only port |
protectKernelDefaults: true | Fail rather than silently changing kernel settings |
rotateCertificates: true | Rotate the kubelet client certificate |
Verification commands
kubectl auth can-i --list --as=system:serviceaccount:shop:default -n shop | What the default ServiceAccount may do (should be nothing useful) |
kubectl run test --image=nginx:1.27 -n shop | Should be rejected under restricted PSA |
kubectl -n shop exec deploy/web -- wget -qO- -T 3 http://api.payments:8080 | Cross-namespace call (should time out) |
kubectl logs job/kube-bench | Benchmark result |