Anurag's Notes

Security

← 06 Cluster Maintenance · Index · 08 Storage →

Contents


7.1 Security Section Introduction

Source: KodeKloud notes

Section map

Course order: security primitives → securing the persistent key-value store (etcd) → authentication and authorization → security contexts → TLS certificates for cluster components → image security → network policies.

AreaCoversPurpose
AuthenticationClient certs, bearer tokens, OIDCEstablishes who is calling (§7.3, §7.10, §7.12)
Authorization (RBAC)Roles, RoleBindings, ClusterRoles, default behaviour, common pitfallsLeast privilege (§7.15–§7.19)
TLS certificatesCertificate roles per component, rotation, troubleshootingSecures component ↔ component and user ↔ API traffic (§7.4–§7.11)
Image securitySigning, admission controllers, private registriesBlocks untrusted images (§7.22)
Security contextsCapabilities, privileged containers, SELinuxLimits process privileges in containers (§7.25)
Network policiesIngress/egress rules, enforcementPod-to-pod segmentation, limits lateral movement (§7.27)
  • Section goals: understand how users and components authenticate. Read and interpret certificate config and logs. Apply least-privilege RBAC. Harden workloads with security contexts and image policies. Restrict traffic with NetworkPolicies.
  • TLS gets extra depth, from basics through to cluster-level troubleshooting. Certificate problems are the most common issue when you build or debug a cluster yourself. If you already know TLS, focus on the Kubernetes-specific parts and on authorization.

7.2 Kubernetes Security Primitives

Source: KodeKloud notes

Host security (control plane and worker nodes)

  • A compromised host gives the attacker a path into workloads and cluster services, so lock down host access first.
  • Disable root login and password-based SSH. Allow SSH key-based auth only.
  • Restrict SSH with allowlists (source IP, bastion host, IAM-based access).
  • OS hardening: SELinux/AppArmor, minimal packages, current kernels.
  • Lock down cloud/virtualisation management consoles and instance metadata APIs.
  • Use centralised logging and host intrusion detection/monitoring.

kube-apiserver: first line of defence

Every operation (kubectl, client libraries, controllers) goes through the kube-apiserver. Securing it means answering two questions:

QuestionMechanismOptions
Who can access?AuthenticationStatic token files, TLS client certificates, external IdP (OIDC, LDAP), ServiceAccounts
What can they do?AuthorizationRBAC, ABAC, Node authorizer, Webhook
Authentication methodTypical useRecommendation
Static token fileLegacy tooling, bootstrap scriptsNot for production. Tokens are never rotated
TLS client certificatesComponents (kubelet, controller-manager…)Standard for component auth. You must manage the cert lifecycle
External IdP (OIDC e.g. Dex/Keycloak, LDAP)Human users, SSOPreferred for users. Central identity, short-lived credentials
ServiceAccountsIn-cluster workloads (pods)Pod identity. Pair with RBAC and limit token scope and lifetime
Authorization modePurposeWhen
RBACRoles bound to users, groups, ServiceAccountsDefault choice for most clusters
ABACPolicies based on request attributesLegacy. Avoid for new clusters
Node authorizerLimits each kubelet to the API objects of its own nodeEnabled to protect node-scoped resources
WebhookExternal authorization decision pointCustom or central policy engines
  • Best practice: narrowly scoped Roles/ClusterRoles. Prefer RoleBinding over ClusterRoleBinding unless you really need cluster-wide rights.

TLS between components

Every control plane and node component talks to kube-apiserver over TLS

  • All traffic between etcd, kube-apiserver, kube-controller-manager, kube-scheduler, kubelet and kube-proxy is TLS-encrypted. Production clusters use mutual TLS (mTLS).
  • Protect etcd with TLS and restricted access. Enable encryption at rest (§5.12).
  • Use mTLS between apiserver ↔ kubelet and between control plane components.
  • Automate certificate rotation and lifecycle.
  • Expose the API server only to trusted networks/load balancers. Use API aggregation and admission controllers (e.g. PodSecurity, OPA/Gatekeeper) for extra enforcement.

Network security between workloads

  • Default: all pods can reach all pods across the cluster. Restrict with NetworkPolicy (details in §7.27).
  • Enforcement is done by the CNI plugin. If the CNI doesn't support NetworkPolicy, policies are accepted but have no effect.
  • Principles: start with default-deny ingress/egress per namespace, then allow only the traffic you need. Select with labels. Test in non-prod. Audit traffic with policy logging or flow exporters.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-frontend-to-backend
  namespace: production
spec:
  podSelector:            # pods this policy protects
    matchLabels:
      app: backend
  policyTypes:
  - Ingress
  ingress:
  - from:
    - podSelector:        # only frontend pods (same namespace) may connect
        matchLabels:
          app: frontend
    ports:
    - protocol: TCP
      port: 8080

[!WARNING] Exam traps / remember:

  • Authentication always runs before authorization.
  • NetworkPolicy objects do nothing without a CNI that enforces them.
  • With no NetworkPolicy selecting a pod, all traffic to and from it is allowed.

7.3 Authentication

Source: KodeKloud notes

Who accesses the cluster

  • Actors: admins, developers, end users, third-party services/bots. End-user auth to apps is handled by the apps themselves and is out of scope. This lesson covers management access to kube-apiserver.
  • Two identity types:
    • Humans (admins, developers): Kubernetes has no User object. You can't create users with kubectl or list them. Identity comes from outside the cluster: static files, X.509 certs, or an IdP (OIDC/LDAP/Kerberos).
    • Robots (processes, controllers, CI/CD, integrations): ServiceAccounts. These are real API objects that the cluster creates and manages (§7.20).
  • Every request (kubectl, SDK, curl) is authenticated by kube-apiserver before authorization and admission:
kubectl get pods --all-namespaces
curl -k https://kube-server-ip:6443/          # -k = skip server cert verification (labs only)

Authentication mechanisms

MechanismUse caseNotes
Static token file (and, historically, basic-auth file)Labs, experiments, bootstrapping--token-auth-file=<csv>. Clear text. Insecure
Client TLS certificatesOperators, automation, kubeletsSigned by the cluster CA. Common in production
External IdP (OIDC, LDAP, Kerberos)Enterprise SSO for humansToken exchange with a corporate IdP
ServiceAccountsPods/controllers needing API accessProjected, time-bound token; since v1.24 no auto-created token Secret (kubectl create token <sa>, §7.20)

Static token file

CSV columns: token,username,uid,"group1,group2" (the group column is optional. Quote it when it holds several groups).

KpjcVbI7rCFAHYPkByTIzRb7gu1cUc4B,user10,u0010,group1
rJjncHmvtXHc6MlWQddhtvNyyhgTdxSC,user11,u0011,group1
mjpOFIEiFoKL9toikaRNtt59ePtczZSq,user12,u0012,group2
PG41IXhs7OjqwWkmBkvgGT9gloyUqZij,user13,u0013,group2
# kube-apiserver flag (systemd ExecStart or static-pod command)
--token-auth-file=/path/to/user-token-details.csv

# use the token as a bearer token
curl -k -H "Authorization: Bearer KpjcVbI7rCFAHYPkByTIzRb7gu1cUc4B" https://master-node-ip:6443/
  • Basic-auth file (concept from the course): maps usernames to plaintext passwords plus uid/groups.
  • kubeadm runs kube-apiserver as a static pod (systemd setups run it as a host process), so the CSV has to be reachable where the apiserver runs. For a static pod, mount it into the container or follow your distribution's method for supplying extra files.

[!NOTE] Beyond the course: a hostPath mount for the token file in a kubeadm static pod:

# /etc/kubernetes/manifests/kube-apiserver.yaml (relevant fragments only)
spec:
  containers:
  - command:
    - kube-apiserver
    - --token-auth-file=/tmp/users/user-token-details.csv   # path as seen inside the container
    volumeMounts:
    - mountPath: /tmp/users
      name: usr-details
      readOnly: true
  volumes:
  - hostPath:
      path: /tmp/users            # directory on the control-plane host holding the CSV
      type: DirectoryOrCreate
    name: usr-details
  • A token only proves identity. The user still needs authorization, e.g. RBAC Role + RoleBinding (§7.16).
  • Production: certificates for machine identities, OIDC/LDAP for humans. Both give better security, auditability and IdP integration.

[!CAUTION] Correction: the basic-auth (password) file flag --basic-auth-file was removed in v1.19. Only --token-auth-file still exists for static credentials, and it is insecure (clear text, no rotation, apiserver restart needed for changes).

[!WARNING] Exam traps / remember:

  • "List all users" is impossible: there is no User resource. ServiceAccounts are the only identities stored in the cluster.
  • Static token and password files store credentials in clear text. Fine for labs, never for production.
  • When you add a flag to a static-pod manifest that points at a file, also add a volume/volumeMount, or the apiserver won't start.

7.4 TLS Introduction

Source: KodeKloud notes

Goals of the TLS lessons

What TLS certificates are · how to generate, view and configure them · how Kubernetes uses them · how to troubleshoot them.

TopicKey skillExample
FundamentalsCertificates, keys, CAs, SANs, trust chainsWhy a CA is needed to validate identity
GeneratingPrivate key → CSR → CA-signed certopenssl req -new -key server.key -out server.csr
InspectingDecode SANs, issuer, expiryopenssl x509 -in cert.pem -text -noout
Kubernetes usageWhere certs live, API authenticationkube-apiserver files under /etc/kubernetes/pki (kubeadm). Certificates are also stored in Secrets
TroubleshootingHostname/SAN mismatch, expired cert/CA, wrong CA chain, key/cert format problemsChecklist to validate quickly
  • Concepts covered: certificate, CA, private key, CSR, SAN, chain.
  • How kube-apiserver, kubelet and ingresses consume certificates.

7.5 TLS Basics

Source: KodeKloud notes

Why TLS

  • Protects data in transit (passwords, tokens, financial data). Without it, credentials cross the network as plaintext and can be sniffed.
  • A certificate establishes trust: it proves the server (or client) is who it claims to be.

Symmetric vs asymmetric encryption

SymmetricAsymmetric
KeysOne shared key encrypts and decryptsKey pair: private key (secret) + public key ("public lock", shared freely)
SpeedFast. Used for bulk dataSlow. Used for authentication and key exchange
Weak pointThe key must reach the other side. Intercept it and the data is exposedNone for distribution: data encrypted with the public key can be decrypted only with the matching private key
Role in TLSEncrypts the session after the handshakeAuthenticates the server and securely agrees the symmetric key

SSH (asymmetric key auth)

ssh-keygen                       # creates id_rsa (private) + id_rsa.pub (public)
cat ~/.ssh/authorized_keys       # on the server: public keys allowed to log in
# ssh-rsa AAAAB3NzaC1yc...KhtUBfoTz1BqR V1NThvOo4opzEwRQo1mWx user1
ssh -i id_rsa user1@server1      # authenticate with your private key
  • The same public key can go on many servers. Other users generate their own pairs, and you add their public keys to authorized_keys.
  • Whoever holds your private key can log in as you.

[!NOTE] Beyond the course: OpenSSH ≥ 9.5 ssh-keygen defaults to Ed25519 (id_ed25519/id_ed25519.pub). Use ssh-keygen -t rsa for the classic id_rsa names.

HTTPS handshake and CA trust

Issuance (key → CSR → CA signs → cert) and the TLS handshake that uses it Diagram (new): PKI in two phases: CA issuance of the server certificate, then the per-connection handshake with symmetric session key.

  1. Client connects and asks for a secure session.
  2. Server sends its certificate (public key + identity).
  3. Client validates the chain against its trust store and checks the hostname against the SANs and the expiry.
  4. Key exchange produces a shared symmetric session key, by either:
    • RSA key transport: the client encrypts a symmetric key with the server's public key, or
    • ephemeral Diffie-Hellman (ECDHE): preferred in modern TLS because it gives forward secrecy.
  5. All further traffic uses the symmetric key.
  • Why signatures matter: anyone can create a certificate that claims to be my-bank.com. A phishing or man-in-the-middle server can present one. Clients only trust certificates signed by a CA in their trust store.
  • Self-signed cert: signed by its own key and not trusted by default. The browser shows "Your connection is not private" (also shown for expired or untrusted-CA certs).
  • Public CAs (Symantec, DigiCert, GlobalSign, Comodo…): their public certs ship with browsers and operating systems.
  • Private/internal CA: for internal sites. Its CA certificate must be distributed to every client (e.g. installed in employee browsers) before clients trust what it issues.
  • Getting a cert: generate key + CSR → send the CSR to the CA → the CA validates domain ownership and signs with the CA's private key → certificate.
openssl genrsa -out my-bank.key 2048                          # private key
openssl req -new -key my-bank.key -out my-bank.csr \
  -subj "/C=US/ST=CA/O=MyOrg Inc./CN=my-bank.com"             # CSR: public key + identity

Fields to read on a certificate (openssl x509 -in my-bank.crt -text -noout, trimmed):

Serial Number: 420327018966204255
Signature Algorithm: sha256WithRSAEncryption
Issuer: CN=<signing CA>
Validity
    Not After : Feb  9 13:41:28 2020 GMT
Subject: CN=my-bank.com
X509v3 Subject Alternative Name:
    DNS:mybank.com, DNS:i-bank.com, DNS:we-bank.com
Subject Public Key Info: ...

mTLS, signing vs encryption, PKI

  • mTLS: the server also requires a client certificate signed by a CA it trusts. Common in internal and enterprise APIs (and throughout Kubernetes), rare on public websites.
  • The two keys of a pair are mathematically linked:
    • Confidentiality: encrypt with the recipient's public key. Only their private key decrypts it.
    • Authenticity/integrity: sign (a hash) with your private key. Anyone verifies with your public key. Signing ≠ encrypting the whole message with the private key. Never share private keys.
  • PKI (Public Key Infrastructure) = the whole system of keys, certificates, CAs, issuance and validation.

File naming convention

Certificates (*.crt, .pem) vs private keys (.key, *-key.pem)

ContentExtensionsExamples
Certificate (public key).crt, .pemserver.crt, server.pem, client.crt, client.pem
Private key.key, -key.pemserver.key, server-key.pem, client.key, client-key.pem
CSR.csrmy-bank.csr

[!WARNING] Exam traps / remember:

  • Rule of thumb: "key" in the file name = private key. No "key" = certificate/public key.
  • A stolen private key lets an attacker impersonate the server. Without forward secrecy, it can also decrypt previously captured sessions. Store keys securely and rotate them if compromised.

7.6 TLS in Kubernetes

Source: KodeKloud notes

Three kinds of certificate

  • Server certificates: on services, to secure their HTTPS endpoints.
  • Root/CA certificates: the CA's own key pair (ca.crt, ca.key). Signs everything else. Distributing ca.crt makes components trust what the CA signs.
  • Client certificates: presented by clients to prove identity (mTLS).
  • Requirement: all component ↔ component and admin ↔ cluster communication is TLS-encrypted and authenticated.

Component → certificate map

Kubernetes certificate layout: CA, client certs (left), server certs (right)

ComponentRoleCert / key (course names)
kube-apiserverServer (serves the API over HTTPS)apiserver.crt / apiserver.key
etcdServer (stores cluster state)etcdserver.crt / etcdserver.key
kubeletServer (HTTPS endpoint on each node that the apiserver calls)kubelet.crt / kubelet.key
admin (kubectl)Client → apiserveradmin.crt / admin.key
kube-schedulerClient → apiserverscheduler.crt / scheduler.key
kube-controller-managerClient → apiservercontroller-manager.crt / controller-manager.key
kube-proxyClient → apiserverkube-proxy.crt / kube-proxy.key
kube-apiserver as clientClient → etcd, client → kubeletapiserver-etcd-client.crt/.key, apiserver-kubelet-client.crt/.key
kubelet as clientClient → apiserverkubelet-client.crt / kubelet-client.key
  • kube-apiserver is the only component that talks to etcd. To etcd it is a client. The apiserver can reuse its serving cert/key for this or get a dedicated client pair. Separate pairs make rotation and auditing easier.
  • Grouping: client certs (users and components → apiserver) and server certs (apiserver, etcd, kubelet).

CAs

  • At least one CA per cluster.
  • Single CA for everything: simpler, less isolation.
  • Multiple CAs: e.g. a dedicated etcd CA that signs only etcd and its clients. Scoped trust, better isolation. kubeadm does this (§7.9).

Next steps / rules

  • Generate the CA (ca.key/ca.crt) → CSRs per component → sign with the CA (§7.7).
  • For hostname/IP verification modern clients only check SANs. Using CN for that is deprecated.
  • Plan rotation, secure storage and revocation.

[!WARNING] Exam traps / remember:

  • The apiserver and kubelet are each both server and client. etcd and kubelet are servers that the apiserver connects to.
  • Private key files (*.key, *-key.pem) must never leak or be committed. If one is compromised, revoke and reissue every certificate that relies on it.

7.7 TLS in Kubernetes Certificate Creation

Source: KodeKloud notes

Tools: OpenSSL (used here), Easy-RSA, CFSSL. Every certificate follows the same flow: genrsa (key) → req -new (CSR with subject) → x509 -req (sign).

1. CA (self-signed root)

openssl genrsa -out ca.key 2048                                       # CA private key
openssl req -new -key ca.key -subj "/CN=KUBERNETES-CA" -out ca.csr    # CA CSR
openssl x509 -req -in ca.csr -signkey ca.key -out ca.crt -days 3650   # self-sign with its own key (10 y)
  • -signkey = self-signed (signed by its own key). All later certs use -CA ca.crt -CAkey ca.key.
  • The root cert should carry Basic Constraints CA:TRUE so that TLS clients accept it as a CA. Set this through an OpenSSL config v3_ca section or -extensions/-extfile. The plain command above does not explicitly add basicConstraints = CA:TRUE; check the generated certificate and the active OpenSSL configuration.
  • Protect ca.key: if it is lost you can't sign new certs. If it leaks, everything it signed must be reissued. Use strict file permissions or a secret store.

[!NOTE] Beyond the course: two ways to get CA:TRUE, both checked with OpenSSL 3.x/4.x:

openssl req -x509 -new -key ca.key -subj "/CN=KUBERNETES-CA" -days 3650 -out ca.crt   # req -x509 adds CA:TRUE by default
openssl x509 -req -in ca.csr -signkey ca.key -out ca.crt -days 3650 \
  -extfile <(printf "basicConstraints=critical,CA:TRUE\nkeyUsage=critical,keyCertSign,cRLSign\n")

2. Client certificates (admin user, components)

openssl genrsa -out admin.key 2048
openssl req -new -key admin.key -subj "/CN=kube-admin/O=system:masters" -out admin.csr   # CN = user name, O = group
openssl x509 -req -in admin.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out admin.crt -days 365
  • CN = the user name the apiserver sees (in audit logs and RBAC). Any name works, e.g. kube-admin / kubernetes-admin.
  • O (organization) = group. system:masters is a built-in group with full cluster-admin rights. This is what separates the admin from an ordinary user.
  • -CAcreateserial creates ca.srl (serial-number file) on first use.
  • The same pattern (key → CSR → sign with the CA) applies to the other clients.

[!NOTE] Beyond the course: system components need the system: prefix in CN so that the built-in RBAC roles match: CN=system:kube-scheduler, CN=system:kube-controller-manager, CN=system:kube-proxy.

[!CAUTION] Correction: the original notes put the group in OU (/CN=kubernetes-admin/OU=system:masters, /CN=system:node:node01/OU=system:nodes). Kubernetes reads groups from the O (Organization) field only. An OU value is ignored, so the cert authenticates but gets no system:masters or system:nodes rights. Use /O=system:masters and /O=system:nodes.

[!NOTE] Beyond the course: since kubeadm v1.29, /etc/kubernetes/admin.conf uses O=kubeadm:cluster-admins (bound to the cluster-admin ClusterRole by a ClusterRoleBinding). The break-glass super-admin.conf keeps O=system:masters. Members of system:masters bypass RBAC entirely, and a cert can't be revoked short of rotating the CA.

Using the cert pair:

curl https://kube-apiserver:6443/api/v1/pods \
  --key admin.key --cert admin.crt --cacert ca.crt          # client key + cert, CA to verify the server
apiVersion: v1
kind: Config
clusters:
- name: kubernetes
  cluster:
    certificate-authority: ca.crt          # CA used to verify the apiserver
    server: https://kube-apiserver:6443
users:
- name: kubernetes-admin
  user:
    client-certificate: admin.crt
    client-key: admin.key
contexts:                                  # added so the file is usable as-is (kubeconfig details: §7.12)
- name: kubernetes-admin@kubernetes
  context:
    cluster: kubernetes
    user: kubernetes-admin
current-context: kubernetes-admin@kubernetes
  • Every component and client needs a copy of ca.crt to verify the peer it talks to.
File(s)Purpose
ca.key, ca.crtRoot CA. Signs all cluster certs
admin.key, admin.crtAdmin client cert (CN = admin user name, O=system:masters)
apiserver.key, apiserver.crtkube-apiserver serving cert. Must include SANs
kubelet.key, kubelet.crtPer-node kubelet serving cert (identifies the node)
etcdserver.key, etcdserver.crtetcd serving cert

3. etcd server (and peer) certificates

  • etcd uses TLS for client ↔ server and peer ↔ peer traffic. In HA clusters each member gets its own peer cert.
  • Enable client-cert auth (mTLS) so that only clients signed by the trusted CA can connect.
FlagPurpose
--cert-file / --key-fileetcd serving cert/key (clients, 2379)
--client-cert-auth=trueRequire client certificates
--trusted-ca-fileCA that validates client certs
--peer-cert-file / --peer-key-fileMember ↔ member cert/key (2380)
--peer-client-cert-auth=trueRequire certs from peers
--peer-trusted-ca-fileCA that validates peer certs
# etcd flags (paths illustrative)
- --advertise-client-urls=https://127.0.0.1:2379
- --key-file=/path-to-certs/etcdserver.key
- --cert-file=/path-to-certs/etcdserver.crt
- --client-cert-auth=true
- --trusted-ca-file=/path-to-certs/ca.crt
- --peer-cert-file=/path-to-certs/etcdpeer1.crt
- --peer-key-file=/path-to-certs/etcdpeer1.key
- --peer-client-cert-auth=true
- --peer-trusted-ca-file=/path-to-certs/ca.crt
- --data-dir=/var/lib/etcd
- --initial-advertise-peer-urls=https://127.0.0.1:2380
- --initial-cluster=master=https://127.0.0.1:2380
- --listen-client-urls=https://127.0.0.1:2379
- --listen-peer-urls=https://127.0.0.1:2380
- --name=master
- --snapshot-count=10000

4. kube-apiserver serving cert (SANs)

  • Clients reach the apiserver by many names and IPs. All of them must be SANs:
    • DNS: kubernetes, kubernetes.default, kubernetes.default.svc, kubernetes.default.svc.cluster.local
    • IPs: the kubernetes Service ClusterIP (first IP of the service CIDR, e.g. 10.96.0.1) and the host/advertise IP (e.g. 172.17.0.87). Add any LB name/IP too.
  • CN = kube-apiserver.
# openssl.cnf
[req]
req_extensions = v3_req
distinguished_name = req_distinguished_name

[req_distinguished_name]

[v3_req]
basicConstraints = CA:FALSE
keyUsage = digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth, clientAuth
subjectAltName = @alt_names

[alt_names]
DNS.1 = kubernetes
DNS.2 = kubernetes.default
DNS.3 = kubernetes.default.svc
DNS.4 = kubernetes.default.svc.cluster.local
IP.1 = 10.96.0.1
IP.2 = 172.17.0.87
openssl genrsa -out apiserver.key 2048
openssl req -new -key apiserver.key -subj "/CN=kube-apiserver" -out apiserver.csr -config openssl.cnf
openssl x509 -req -in apiserver.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
  -out apiserver.crt -extensions v3_req -extfile openssl.cnf -days 1000   # SANs are applied at signing time

[!CAUTION] Correction: the original openssl.cnf fragment references distinguished_name = req_distinguished_name but has no [req_distinguished_name] section. OpenSSL then fails with "Unable to get 'req_distinguished_name' section". An empty section (as above) fixes it. Also, openssl x509 -req does not copy extensions from the CSR, so the -extensions v3_req -extfile openssl.cnf at signing time is what puts the SANs into the certificate.

5. kube-apiserver flags that reference certs

ExecStart=/usr/local/bin/kube-apiserver \
  --advertise-address=${INTERNAL_IP} \
  --allow-privileged=true \
  --apiserver-count=3 \
  --authorization-mode=Node,RBAC \
  --bind-address=0.0.0.0 \
  --enable-swagger-ui=true \
  --etcd-cafile=/var/lib/kubernetes/ca.crt \
  --etcd-certfile=/var/lib/kubernetes/apiserver-etcd-client.crt \
  --etcd-keyfile=/var/lib/kubernetes/apiserver-etcd-client.key \
  --etcd-servers=https://127.0.0.1:2379 \
  --event-ttl=1h \
  --kubelet-certificate-authority=/var/lib/kubernetes/ca.crt \
  --kubelet-client-certificate=/var/lib/kubernetes/apiserver-kubelet-client.crt \
  --kubelet-client-key=/var/lib/kubernetes/apiserver-kubelet-client.key \
  --kubelet-https=true \
  --runtime-config=api/all \
  --service-account-key-file=/var/lib/kubernetes/service-account.pem \
  --service-cluster-ip-range=10.32.0.0/24 \
  --service-node-port-range=30000-32767 \
  --client-ca-file=/var/lib/kubernetes/ca.crt \
  --tls-cert-file=/var/lib/kubernetes/apiserver.crt \
  --tls-private-key-file=/var/lib/kubernetes/apiserver.key \
  --v=2
PurposeFlag(s)
CA to verify clients (users, components)--client-ca-file
Own serving cert/key--tls-cert-file, --tls-private-key-file
As client → etcd--etcd-cafile, --etcd-certfile, --etcd-keyfile, --etcd-servers
As client → kubelet--kubelet-certificate-authority, --kubelet-client-certificate, --kubelet-client-key

[!CAUTION] Correction (outdated flags): --kubelet-https and --enable-swagger-ui have been removed from current kube-apiserver releases, and --apiserver-count is deprecated. Don't copy them into a modern manifest. Current apiservers also require --service-account-issuer and --service-account-signing-key-file (see the kubeadm manifest in §7.9).

6. kubelet certificates (per node)

  • Server cert (apiserver → kubelet HTTPS): one per node (node01, node02, node03), named after the node (CN or SAN). Pass it with kubelet flags.
  • Client cert (kubelet → apiserver): CN = system:node:<nodeName>, O = system:nodes, e.g. /CN=system:node:node01/O=system:nodes. The apiserver needs this format to give the kubelet node permissions through the Node authorizer.
  • Put the client cert/key and ca.crt into each kubelet's kubeconfig.

Per-node kubelet client certificates: CN system:node:nodeXX, group SYSTEM:NODES

7. Inspect

openssl x509 -in apiserver.crt -text -noout      # cert: subject, issuer, validity, SANs
openssl req  -in apiserver.csr -text -noout      # CSR: check subject/SANs before signing

[!WARNING] Exam traps / remember:

  • Self-sign the CA with -signkey ca.key. Sign everything else with -CA ca.crt -CAkey ca.key.
  • Group = O, user = CN. Admin → O=system:masters. Nodes → CN=system:node:<name>, O=system:nodes. System components → CN=system:kube-scheduler and the like.
  • A missing SAN (e.g. a new LB name) → "x509: certificate is valid for …, not …". Re-issue with the SAN added.
  • Distribute ca.crt everywhere. Keep *.key private. Rotate before expiry and keep an inventory.

[!NOTE] Beyond the course: kubelet serving-certificate flags are --tls-cert-file / --tls-private-key-file; the KubeletConfiguration fields are tlsCertFile / tlsPrivateKeyFile.

7.8 View Certificate Details

Source: KodeKloud notes

Step 1: how was the cluster built?

ProvisioningControl plane runs asWhere to find cert pathsLogs
"The hard way" (manual)systemd services/etc/systemd/system/kube-apiserver.service (ExecStart flags)journalctl -u etcd.service -l
kubeadmStatic pods/etc/kubernetes/manifests/kube-apiserver.yaml (container command). Certs in /etc/kubernetes/pkikubectl logs <pod> -n kube-system. If the apiserver is down: crictl ps -a, crictl logs <id>
cat /etc/systemd/system/kube-apiserver.service      # manual setup
cat /etc/kubernetes/manifests/kube-apiserver.yaml   # kubeadm
# manual setup: cert-related ExecStart flags (excerpt)
ExecStart=/usr/local/bin/kube-apiserver \
  --advertise-address=172.17.0.32 \
  --authorization-mode=Node,RBAC \
  --client-ca-file=/var/lib/kubernetes/ca.pem \
  --etcd-cafile=/var/lib/kubernetes/ca.pem \
  --etcd-certfile=/var/lib/kubernetes/kubernetes.pem \
  --etcd-keyfile=/var/lib/kubernetes/kubernetes-key.pem \
  --kubelet-certificate-authority=/var/lib/kubernetes/ca.pem \
  --kubelet-client-certificate=/var/lib/kubernetes/kubernetes.pem \
  --kubelet-client-key=/var/lib/kubernetes/kubernetes-key.pem \
  --tls-cert-file=/var/lib/kubernetes/kubernetes.pem \
  --tls-private-key-file=/var/lib/kubernetes/kubernetes-key.pem \
  -v=2
  • In this manual setup a single pair (kubernetes.pem/kubernetes-key.pem) acts as the serving cert and the etcd/kubelet client cert: one cert reused for several roles.
# kubeadm: /etc/kubernetes/manifests/kube-apiserver.yaml (excerpt)
spec:
  containers:
  - command:
    - kube-apiserver
    - --authorization-mode=Node,RBAC
    - --advertise-address=172.17.0.32
    - --client-ca-file=/etc/kubernetes/pki/ca.crt
    - --etcd-cafile=/etc/kubernetes/pki/etcd/ca.crt
    - --etcd-certfile=/etc/kubernetes/pki/apiserver-etcd-client.crt
    - --etcd-keyfile=/etc/kubernetes/pki/apiserver-etcd-client.key
    - --kubelet-client-certificate=/etc/kubernetes/pki/apiserver-kubelet-client.crt
    - --kubelet-client-key=/etc/kubernetes/pki/apiserver-kubelet-client.key
    - --proxy-client-cert-file=/etc/kubernetes/pki/front-proxy-client.crt
    - --proxy-client-key-file=/etc/kubernetes/pki/front-proxy-client.key
    - --requestheader-client-ca-file=/etc/kubernetes/pki/front-proxy-ca.crt
    - --tls-cert-file=/etc/kubernetes/pki/apiserver.crt
    - --tls-private-key-file=/etc/kubernetes/pki/apiserver.key

Step 2: build a certificate inventory (spreadsheet)

For every cert record: path, CN, SANs (alt names), Organization (O), Issuer, Expiration (UTC), purpose. This shows certs that expire soon or were issued by the wrong CA.

PathCNALT namesOIssuerExpiry
/etc/kubernetes/pki/apiserver.crtkube-apiserverDNS:master, kubernetes, kubernetes.default, kubernetes.default.svc, IP:10.96.0.1, IP:172.17.0.27kubernetesFeb 11 05:39:20 2020
/etc/kubernetes/pki/ca.crtkuberneteskubernetesFeb 8 05:39:19 2029
/etc/kubernetes/pki/apiserver-kubelet-client.crtkube-apiserver-kubelet-clientsystem:masterskubernetesFeb 11 05:39:20 2020
/etc/kubernetes/pki/apiserver-etcd-client.crtkube-apiserver-etcd-clientsystem:mastersselfFeb 11 05:39:22 2020
/etc/kubernetes/pki/etcd/ca.crtkuberneteskubernetesFeb 8 05:39:21 2017

[!CAUTION] Correction: the original inventory’s last two rows misidentify the etcd CA and issuer: kubeadm uses CN etcd-ca, with a 10-year CA lifetime, and apiserver-etcd-client.crt is issued by that CA rather than self-signed. The 2017 expiry in the source is a typo. Inspect the actual certificates (§7.9).

Kubernetes docs PKI tables: default CN, parent CA, O, kind, SANs; key/cert paths and the flags that use them

  • Reference for expected CN, parent CA, O, kind and SANs per cert, plus the recommended paths and the flag that consumes each one: Kubernetes PKI best practices.

Step 3: decode each cert

openssl x509 -in /etc/kubernetes/pki/apiserver.crt -text -noout
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number: 3147495682089747350 (0x2bae26a58f090396)
    Signature Algorithm: sha256WithRSAEncryption
        Issuer: CN=kubernetes                          <- signing CA
        Validity
            Not Before: Feb 11 05:39:19 2019 GMT
            Not After : Feb 11 05:39:20 2020 GMT       <- expiry
        Subject: CN=kube-apiserver                     <- identity (and O= groups, if any)
    X509v3 extensions:
        X509v3 Key Usage: critical
            Digital Signature, Key Encipherment
        X509v3 Extended Key Usage:
            TLS Web Server Authentication              <- server vs client cert
    X509v3 Subject Alternative Name:
        DNS:master, DNS:kubernetes, DNS:kubernetes.default,
        DNS:kubernetes.default.svc, DNS:kubernetes.default.svc.cluster.local,
        IP Address:10.96.0.1, IP Address:172.17.0.27

Audit checklist

  • SANs cover every hostname/IP clients use: service ClusterIP, service DNS names, node IPs.
  • Issuer is the expected CA (kubeadm CA or the organisation's CA).
  • Validity: not expired, and the renewal policy fits your guidelines. An expired cert fails the TLS handshake immediately.
  • Key usage / EKU fit the role (server vs client).
  • Subject O fits the usage (e.g. system:masters only on privileged clients).

Step 4: logs for TLS failures

journalctl -u etcd.service -l                 # systemd-managed
kubectl logs etcd-controlplane -n kube-system # kubeadm static pod (pod name = etcd-<node>)
crictl ps -a                                  # apiserver/etcd down → kubectl fails → go to the runtime
crictl logs <container-id>
2019-02-13 02:53:28.185588 I | embed: ClientTLS: cert = /etc/kubernetes/pki/etcd/server.crt, key = /etc/kubernetes/pki/etcd/server.key, trusted-ca = /etc/kubernetes/pki/etcd/old-ca.crt, client-cert-auth = true
WARNING: 2019/02/13 02:53:30 Failed to dial 127.0.0.1:2379: connection error: desc = "transport: authentication handshake failed: remote error: tls: bad certificate"; please retry.
  • tls: bad certificate = the presented cert failed validation: SAN/CN mismatch, wrong issuer/CA (here trusted-ca points at old-ca.crt), or expired.

[!NOTE] Beyond the course (kubeadm cert lifecycle, exam-relevant):

kubeadm certs check-expiration                      # expiry + issuing CA of every kubeadm-managed cert/kubeconfig
kubeadm certs renew all                             # or: kubeadm certs renew apiserver
# then restart the control-plane static pods (e.g. move manifests out of /etc/kubernetes/manifests and back)
openssl x509 -in /etc/kubernetes/pki/apiserver.crt -noout -subject -issuer -enddate
openssl x509 -in /etc/kubernetes/pki/apiserver.crt -noout -ext subjectAltName

kubeadm leaf certs are valid for 1 year, CAs for 10 years. A kubeadm upgrade apply also renews them.

[!WARNING] Exam traps / remember:

  • Identify the setup first (systemd vs static pods). The command and log location depend on it.
  • Hostname checks use SANs, not CN.
  • If kube-apiserver is down, kubectl logs doesn't work either. Use crictl ps -a / crictl logs, or journalctl -u kubelet.

7.9 Lab Solution View Certification Details

Source: KodeKloud notes

kubeadm PKI: kubernetes CA, etcd CA, front-proxy CA, their leaf certs and the flags that reference them Diagram (new): kubeadm's three CAs, which certificates each one signs, and which kube-apiserver/etcd flags point at each file.

Drill: find the files (answers come from the manifests)

cat /etc/kubernetes/manifests/kube-apiserver.yaml   # or: grep -E 'crt|key|ca' on it
cat /etc/kubernetes/manifests/etcd.yaml
ls /etc/kubernetes/pki /etc/kubernetes/pki/etcd
Lab questionFlag to look atAnswer (kubeadm)
Cert the kube-apiserver serves with--tls-cert-file/etc/kubernetes/pki/apiserver.crt (key: --tls-private-key-file → apiserver.key)
Cert the apiserver uses as a client to etcd--etcd-certfile/etc/kubernetes/pki/apiserver-etcd-client.crt (key --etcd-keyfile → apiserver-etcd-client.key)
CA the apiserver uses to verify etcd--etcd-cafile/etc/kubernetes/pki/etcd/ca.crt
Key the apiserver uses as a client to kubelets--kubelet-client-key/etc/kubernetes/pki/apiserver-kubelet-client.key (cert --kubelet-client-certificate → .crt)
CA for verifying client certs--client-ca-file/etc/kubernetes/pki/ca.crt
Front-proxy (aggregation)--proxy-client-cert-file, --proxy-client-key-file, --requestheader-client-ca-filefront-proxy-client.crt/.key, front-proxy-ca.crt
ServiceAccount token keys--service-account-key-file, --service-account-signing-key-filesa.pub, sa.key
etcd server cert / keyetcd --cert-file / --key-file/etc/kubernetes/pki/etcd/server.crt / server.key
etcd CA (root)etcd --trusted-ca-file (and --peer-trusted-ca-file)/etc/kubernetes/pki/etcd/ca.crt
etcd peer cert / key--peer-cert-file / --peer-key-file/etc/kubernetes/pki/etcd/peer.crt / peer.key

kube-apiserver manifest from the lab (excerpt, kubeadm v1.20):

spec:
  containers:
  - command:
    - kube-apiserver
    - --advertise-address=10.46.98.9
    - --allow-privileged=true
    - --authorization-mode=Node,RBAC
    - --client-ca-file=/etc/kubernetes/pki/ca.crt
    - --enable-admission-plugins=NodeRestriction
    - --enable-bootstrap-token-auth=true
    - --etcd-cafile=/etc/kubernetes/pki/etcd/ca.crt
    - --etcd-certfile=/etc/kubernetes/pki/apiserver-etcd-client.crt
    - --etcd-keyfile=/etc/kubernetes/pki/apiserver-etcd-client.key
    - --etcd-servers=https://127.0.0.1:2379
    - --insecure-port=0
    - --kubelet-client-certificate=/etc/kubernetes/pki/apiserver-kubelet-client.crt
    - --kubelet-client-key=/etc/kubernetes/pki/apiserver-kubelet-client.key
    - --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname
    - --proxy-client-cert-file=/etc/kubernetes/pki/front-proxy-client.crt
    - --proxy-client-key-file=/etc/kubernetes/pki/front-proxy-client.key
    - --requestheader-allowed-names=front-proxy-client
    - --requestheader-client-ca-file=/etc/kubernetes/pki/front-proxy-ca.crt
    - --secure-port=6443
    - --service-account-issuer=https://kubernetes.default.svc.cluster.local
    - --service-account-key-file=/etc/kubernetes/pki/sa.pub
    - --service-account-signing-key-file=/etc/kubernetes/pki/sa.key
    - --service-cluster-ip-range=10.96.0.0/12
    - --tls-cert-file=/etc/kubernetes/pki/apiserver.crt
    - --tls-private-key-file=/etc/kubernetes/pki/apiserver.key
    image: k8s.gcr.io/kube-apiserver:v1.20.0

etcd manifest from the lab (excerpt):

spec:
  containers:
  - command:
    - etcd
    - --advertise-client-urls=https://10.46.98.9:2379
    - --cert-file=/etc/kubernetes/pki/etcd/server.crt
    - --client-cert-auth=true
    - --data-dir=/var/lib/etcd
    - --initial-advertise-peer-urls=https://10.46.98.9:2380
    - --initial-cluster=controlplane=https://10.46.98.9:2380
    - --key-file=/etc/kubernetes/pki/etcd/server.key
    - --listen-client-urls=https://127.0.0.1:2379,https://10.46.98.9:2379
    - --listen-metrics-urls=http://127.0.0.1:2381
    - --listen-peer-urls=https://10.46.98.9:2380
    - --name=controlplane
    - --peer-cert-file=/etc/kubernetes/pki/etcd/peer.crt
    - --peer-client-cert-auth=true
    - --peer-key-file=/etc/kubernetes/pki/etcd/peer.key
    - --peer-trusted-ca-file=/etc/kubernetes/pki/etcd/ca.crt
    - --trusted-ca-file=/etc/kubernetes/pki/etcd/ca.crt
    image: k8s.gcr.io/etcd:3.4.13-0
  • Ports: apiserver 6443. etcd clients 2379, peers 2380, metrics 2381 (plain http, localhost).
  • /etc/kubernetes/pki/etcd/ holds: ca.crt ca.key healthcheck-client.crt healthcheck-client.key peer.crt peer.key server.crt server.key.

[!CAUTION] Correction (outdated lab output): --insecure-port was removed from kube-apiserver in v1.24, so it is absent from current manifests. Images now come from registry.k8s.io (the k8s.gcr.io registry is frozen).

Drill: read cert details (openssl x509 -in <crt> -text -noout)

QuestionWhere in outputLab answer
CN of the apiserver certSubject:CN = kube-apiserver
CA that issued the apiserver certIssuer:CN = kubernetes
Which alias is not in the apiserver cert?X509v3 Subject Alternative NameAnything absent from the list, e.g. kube-master. Present: controlplane, kubernetes, kubernetes.default, kubernetes.default.svc, kubernetes.default.svc.cluster.local, 10.96.0.1, 10.46.98.9
CN of the etcd server certSubject: of etcd/server.crtCN = controlplane (the node name)
Issuer of the etcd server certIssuer:CN = etcd-ca
Validity of the apiserver certNot Before / Not After1 year (Apr 17 2022 → Apr 17 2023)
Validity of the root CA (ca.crt)Not After10 years
openssl x509 -in /etc/kubernetes/pki/apiserver.crt -text -noout
openssl x509 -in /etc/kubernetes/pki/etcd/server.crt -text -noout
openssl x509 -in /etc/kubernetes/pki/ca.crt -text -noout | grep -A2 Validity

Drill: troubleshooting scenario 1 (kubectl: "connection refused")

The connection to the server was refused - did you specify the right host or port?
  1. The apiserver is a static pod, so check it through the runtime:
    crictl ps -a | grep kube-apiserver        # Exited / restarting?
    crictl logs <kube-apiserver-container-id>
  2. apiserver log shows it can't reach etcd:
    grpc: addrConn.createTransport failed to connect to {https://127.0.0.1:2379 ...}. Err: connection error: desc = "transport: Error while dialing dial tcp 127.0.0.1:2379: connect: connection refused". Reconnecting...
    → etcd isn't listening. Check etcd: crictl ps -a | grep etcd, crictl logs <etcd-id>.
  3. etcd log:
    embed: ClientTLS: cert = /etc/kubernetes/pki/etcd/server-certificate.crt, key = /etc/kubernetes/pki/etcd/server.key, ...
    C | etcdmain: open /etc/kubernetes/pki/etcd/server-certificate.crt: no such file or directory
  4. ls /etc/kubernetes/pki/etcd shows the file is really server.crt. Fix: in /etc/kubernetes/manifests/etcd.yaml set --cert-file=/etc/kubernetes/pki/etcd/server.crt and save. The kubelet notices the change and recreates the static pod. etcd binds 127.0.0.1:2379 and 10.46.98.9:2379, the apiserver reconnects, and kubectl works again.

Drill: troubleshooting scenario 2 ("TLS handshake timeout")

Unable to connect to the server: net/http: TLS handshake timeout
  1. etcd logs are full of rejections:
    embed: rejected connection from "127.0.0.1:39626" (error "remote error: tls: bad certificate", ServerName "")
    → mTLS trust mismatch between the apiserver and etcd.
  2. The apiserver manifest has --etcd-cafile=/etc/kubernetes/pki/ca.crt. That is the Kubernetes CA, not the CA that signed etcd's serving cert.
  3. Fix in /etc/kubernetes/manifests/kube-apiserver.yaml:
        - --etcd-cafile=/etc/kubernetes/pki/etcd/ca.crt
        - --etcd-certfile=/etc/kubernetes/pki/apiserver-etcd-client.crt
        - --etcd-keyfile=/etc/kubernetes/pki/apiserver-etcd-client.key
        - --etcd-servers=https://127.0.0.1:2379
    Save → the kubelet restarts the apiserver → handshakes succeed and kubectl comes back.

Debug checklist (apiserver ↔ etcd)

  • Check both manifests: /etc/kubernetes/manifests/kube-apiserver.yaml and etcd.yaml.
  • Every referenced path must exist (ls /etc/kubernetes/pki, ls /etc/kubernetes/pki/etcd). A wrong path stops the static pod from starting, and the container log (or kubelet log) names the missing file.
  • mTLS alignment: etcd serves etcd/server.crt and trusts etcd/ca.crt. The apiserver must use --etcd-cafile=/etc/kubernetes/pki/etcd/ca.crt and present apiserver-etcd-client.crt/.key (signed by the etcd CA).
  • Compare the Issuer: of a cert with the CA file the other side is configured with. A mismatch is the classic cause.
SymptomMeaning
dial tcp 127.0.0.1:2379: connect: connection refusedetcd not running/listening → etcd logs, check its manifest paths
TLS handshake timeout / tls: bad certificateCA mismatch or invalid/expired client cert → compare issuers and CA files
no such file or directory on a .crt/.keyTypo in a manifest path

[!CAUTION] Correction: the lab uses docker ps -a | grep kube-apiserver and docker logs <id>. Since dockershim was removed (v1.24), nodes run containerd/CRI-O, so use crictl ps -a and crictl logs <id>. The flags are the same.

[!NOTE] Beyond the course:

  • After editing a static-pod manifest, wait. The kubelet recreates the pod by itself (watch with watch crictl ps). If nothing appears, check journalctl -u kubelet for YAML or path errors.
  • Back up a manifest outside /etc/kubernetes/manifests before editing it. A copy inside the directory gets run as a second static pod.
  • x509: certificate signed by unknown authority → compare the peer certificate’s Issuer: with the configured CA; for etcd use etcd/ca.crt, not the Kubernetes ca.crt.
  • Inspect the etcd server cert with openssl x509 -in /etc/kubernetes/pki/etcd/server.crt -text -noout. Course sample: Apr 17 2022 → Apr 17 2023, SANs controlplane, kubernetes, kubernetes.default, kubernetes.default.svc, kubernetes.default.svc.cluster.local, IPs 10.96.0.1, 10.46.98.9. Treat these as sample output, not required SANs for every etcd server.
  • etcd static-Pod metadata: name etcd, namespace kube-system, labels component=etcd,tier=control-plane; find it with kubectl -n kube-system get pods -l component=etcd.

PKI map legend: blue marks serving certificates; red marks signing CAs/client certificates. This diagram uses certificate type, rather than component role, for colour.


7.10 Certificates API

Source: KodeKloud notes

Why a Certificates API

  • The cluster signing CA (CA private key + root cert) is the crown jewel: whoever holds the key can mint certs for any identity, including privileged ones. Keep it on a dedicated CA server or in a key-management solution.
  • kubeadm keeps the CA pair on the control-plane node (/etc/kubernetes/pki/ca.crt, ca.key), so that node effectively is the CA server.
  • Signing CSRs by hand (openssl x509 -req -CA ..., see §7.7) does not scale. The Certificates API makes it an auditable workflow: CSRs become API objects that admins review, approve or deny, and the cluster signs approved ones automatically.

Workflow

  1. User generates a private key and a CSR locally (the key never leaves the user).
  2. User (or admin) submits the CSR as a CertificateSigningRequest object.
  3. Admins list and review pending CSRs.
  4. Admin approves (or denies) with kubectl.
  5. If approved and the controller manager has the CA, the cluster signs it and stores the cert, base64-encoded, in status.certificate.
  6. Extract the cert and hand it, together with the key, to the user (client auth / TLS).

Certificates API flow: user creates CSR object, admin reviews and approves, kube-controller-manager signs with cluster CA, cert extracted from status Diagram (new): CSR lifecycle from key generation to extracted certificate, and where the CA key is used.

Commands: key, CSR, object

openssl genrsa -out jane.key 2048                                   # private key
openssl req -new -key jane.key -subj "/CN=jane/O=dev-team" -out jane.csr   # CN = username, O = group
cat jane.csr | base64 -w0                                           # single-line base64 for spec.request
# jane-csr.yaml
apiVersion: certificates.k8s.io/v1
kind: CertificateSigningRequest
metadata:
  name: jane-csr
spec:
  signerName: kubernetes.io/kube-apiserver-client   # client cert for authenticating to the API server
  expirationSeconds: 600                            # optional: requested cert lifetime in seconds
  usages:
  - client auth
  request: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURSBSRVFVRVNULS0tLS0K...   # base64 of the whole PEM CSR, one line
kubectl apply -f jane-csr.yaml
kubectl get csr                       # NAME AGE SIGNERNAME REQUESTOR REQUESTEDDURATION CONDITION -> Pending
kubectl describe csr jane-csr
kubectl certificate approve jane-csr  # or: kubectl certificate deny jane-csr
kubectl get csr jane-csr -o yaml      # status.conditions[type=Approved, reason=KubectlApprove] + status.certificate
kubectl get csr jane-csr -o jsonpath='{.status.certificate}' | base64 --decode > jane.crt
  • After approval, status.conditions gets type: Approved, status: "True", reason: KubectlApprove; status.certificate is filled once signed (CONDITION Approved,Issued).
  • Give jane.key + jane.crt to the user (use in kubeconfig, §7.12).

signerName and usages

PurposesignerNameusages
Client cert for API access (users)kubernetes.io/kube-apiserver-clientclient auth
Serving cert (TLS endpoint)see correction belowserver auth
General-purpose (cluster provisioners)cluster-specific / custom signersclient auth, server auth, key encipherment, digital signature

[!CAUTION] Correction: the notes name kubernetes.io/kube-apiserver-serving as a serving signer. Kubernetes has no such built-in signer. The built-in signers are kubernetes.io/kube-apiserver-client, kubernetes.io/kube-apiserver-client-kubelet (kubelet client certs, can be auto-approved), kubernetes.io/kubelet-serving (kubelet serving certs, never auto-approved) and kubernetes.io/legacy-unknown (legacy v1beta1 only; cannot be requested using certificates.k8s.io/v1 and was never auto-approved). For users authenticating to the API, always use kubernetes.io/kube-apiserver-client with client auth.

Where signing happens

  • All Certificates API automation lives in kube-controller-manager controllers. They expose pending CSRs for review, can auto-approve some CSRs by configured rules, and sign approved CSRs.
  • Signing requires the CA files on the controller manager:
    • --cluster-signing-cert-file (CA cert)
    • --cluster-signing-key-file (CA key)
  • Anyone who can read those files or change controller-manager flags/startup files can mint identities, so lock down access to them.
ActionCommand
Create CSR objectkubectl apply -f jane-csr.yaml
List CSRskubectl get csr
View detailskubectl describe csr jane-csr
Approvekubectl certificate approve jane-csr
Denykubectl certificate deny jane-csr
Extract certkubectl get csr jane-csr -o jsonpath='{.status.certificate}' | base64 --decode > jane.crt

[!WARNING] Exam traps / remember:

  • spec.request = base64 of the entire PEM file (including the BEGIN/END lines), on one line (base64 -w0 or | tr -d '\n'). A multi-line value breaks the object.
  • signerName is mandatory in certificates.k8s.io/v1. usages must match the signer (client auth for kube-apiserver-client).
  • Approval alone does not mean issued: CONDITION goes Pending → Approved → Approved,Issued. status.certificate is base64, so decode it.
  • CSRs are cluster-scoped (no -n).
  • Signing is done by kube-controller-manager, using --cluster-signing-cert-file / --cluster-signing-key-file.

[!NOTE] Beyond the course:

  • expirationSeconds minimum is 600 (10 min). Unset means the signer default, which kube-controller-manager caps with --cluster-signing-duration (default 1 year).
  • On kubeadm the flags are in /etc/kubernetes/manifests/kube-controller-manager.yaml and point to /etc/kubernetes/pki/ca.crt and /etc/kubernetes/pki/ca.key.
  • Build the manifest in one go with a heredoc:
    cat <<EOF | kubectl apply -f -
    apiVersion: certificates.k8s.io/v1
    kind: CertificateSigningRequest
    metadata:
      name: jane-csr
    spec:
      signerName: kubernetes.io/kube-apiserver-client
      usages: ["client auth"]
      request: $(base64 -w0 < jane.csr)
    EOF

7.11 Lab Solution Certificates API

Source: KodeKloud notes

Scenario: new team member Akshay needs access. His CSR and key are on the control plane at /root/akshay.csr and /root/akshay.key. Theory: §7.10.

TaskCommand / answerGotcha
Find the filespwd; ls → akshay.csr akshay.key in /root—
View the CSRcat akshay.csr → PEM -----BEGIN CERTIFICATE REQUEST-----—
Encode for spec.requestGNU: cat akshay.csr | base64 -w 0 · macOS/BSD: cat akshay.csr | base64 | tr -d '\n'Must be a single line
Create CSR object akshaywrite akshay.yaml (below) → kubectl apply -f akshay.yamlsignerName: kubernetes.io/kube-apiserver-client, usages: [client auth]
Status of new CSR?kubectl get csr → CONDITION Pending, REQUESTOR kubernetes-admin—
Approve itkubectl certificate approve akshay → kubectl get csr akshay shows Approved,Issued—
Save issued certkubectl get csr akshay -o jsonpath='{.status.certificate}' | base64 -d > akshay.crt (macOS: base64 -D)—
Inspect certopenssl x509 -in akshay.crt -text -noout—
Who requested the other CSR / which groups?kubectl get csr agent-smith -o yaml → spec.username: agent-x, spec.groups: [system:masters, system:authenticated], usages digital signature, key encipherment, server authsystem:masters = full cluster-admin. Never approve it for an untrusted requester
Reject itkubectl certificate deny agent-smith → CONDITION Denied—
Remove itkubectl delete csr agent-smithDeny keeps the object; delete removes it
# akshay.yaml
apiVersion: certificates.k8s.io/v1
kind: CertificateSigningRequest
metadata:
  name: akshay
spec:
  request: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1J...   # output of base64 -w 0
  signerName: kubernetes.io/kube-apiserver-client
  usages:
  - client auth
  • Usages reflect the purpose: client auth, server auth, digital signature, etc.
  • After cleanup kubectl get csr still lists csr-8mfn8, signer kubernetes.io/kube-apiserver-client-kubelet, requestor system:node:controlplane, Approved,Issued. That is the node's own kubelet client CSR, created and auto-approved when the node joined. Leave it alone.
  • Useful fields to check before approving: spec.groups, spec.usages, spec.signerName, spec.username.

[!CAUTION] Correction: the lab text says approving makes "the API server" sign the certificate. The signer for kubernetes.io/kube-apiserver-client is the CSR signing controller in kube-controller-manager, using --cluster-signing-cert-file / --cluster-signing-key-file (§7.10).

[!WARNING] Exam traps / remember:

  • Check spec.groups before approving. system:masters in a CSR is a red flag, so deny it.
  • base64 -d (GNU) vs base64 -D (BSD/macOS). The exam terminal is Linux, so use -d / -w 0.

7.12 KubeConfig

Source: KodeKloud notes

Without a kubeconfig

# raw REST call with client cert auth
curl https://my-kube-playground:6443/api/v1/pods \
  --key admin.key --cert admin.crt --cacert ca.crt

# same credentials passed to kubectl as flags
kubectl get pods \
  --server=https://my-kube-playground:6443 \
  --client-key=admin.key \
  --client-certificate=admin.crt \
  --certificate-authority=ca.crt
  • Typing these every time is tedious, so put them in a kubeconfig file.
  • Default location: ~/.kube/config ($HOME/.kube/config). Override with --kubeconfig=<path> or the KUBECONFIG env var.

Structure: clusters, users, contexts

SectionHoldsFields
clustersAPI server endpoint + CAserver, certificate-authority or certificate-authority-data
usersclient credentialsclient-certificate / client-key or client-certificate-data / client-key-data
contextswhich user talks to which cluster (+ default namespace)cluster, user, namespace (optional)
current-context (top level)default context kubectl usescontext name
  • In the curl example: server + ca.crt → clusters; admin.key/admin.crt → users; a context my-kube-admin@my-kube-playground ties them together.
  • A kubeconfig only tells kubectl which credentials to present to which server. Adding a user or context creates nothing in the cluster and grants no permissions.
  • Context names are free text. user@cluster is just a convention.
apiVersion: v1
kind: Config
current-context: dev-user@google        # default context
clusters:
- name: my-kube-playground
  cluster:
    server: https://my-kube-playground:6443
    certificate-authority: /path/to/ca.crt
- name: development
  cluster:
    server: https://dev.example.com:6443
    certificate-authority: /path/to/dev-ca.crt
- name: production
  cluster:
    server: https://prod.example.com:6443
    certificate-authority: /path/to/prod-ca.crt
- name: google                            # added: the context below references it
  cluster:
    server: https://google.example.com:6443
    certificate-authority: /path/to/google-ca.crt
contexts:
- name: my-kube-admin@my-kube-playground
  context:
    cluster: my-kube-playground           # must match a clusters[].name
    user: my-kube-admin                   # must match a users[].name
- name: dev-user@google
  context:
    cluster: google
    user: dev-user
- name: prod-user@production
  context:
    cluster: production
    user: prod-user
users:
- name: my-kube-admin
  user:
    client-certificate: /path/to/admin.crt
    client-key: /path/to/admin.key
- name: dev-user
  user:
    client-certificate: /path/to/dev.crt
    client-key: /path/to/dev.key
- name: prod-user
  user:
    client-certificate: /path/to/prod.crt
    client-key: /path/to/prod.key

(The original example's dev-user@google context referenced a google cluster that was never defined. A cluster entry has been added here so the file is valid.)

View and switch

kubectl config view                                  # current kubeconfig; *-data shown as REDACTED (DATA+OMITTED)
kubectl config view --kubeconfig=my-custom-config    # inspect another file
kubectl config use-context prod-user@production      # rewrites current-context in the file
kubectl config --help                                # set-cluster, set-credentials, set-context, delete-context, ...

Namespace in a context

apiVersion: v1
kind: Config
clusters:
- name: production
  cluster:
    server: https://172.17.0.51:6443
    certificate-authority: /etc/kubernetes/pki/ca.crt
contexts:
- name: admin@production
  context:
    cluster: production
    user: admin
    namespace: finance                 # kubectl without -n uses "finance" in this context
users:
- name: admin
  user:
    client-certificate: /etc/kubernetes/pki/admin.crt
    client-key: /etc/kubernetes/pki/admin.key

Certificate paths vs inline data

StyleFieldsValue
File pathcertificate-authority, client-certificate, client-keypath, e.g. /etc/kubernetes/pki/ca.crt (use full paths)
Inlinecertificate-authority-data, client-certificate-data, client-key-database64 of the file content, single line
base64 -w0 /etc/kubernetes/pki/ca.crt > ca.crt.base64        # encode for *-data
echo 'LS0tLS1...rest_of_base64...' | base64 --decode > ca.crt # decode *-data back to PEM
  • Inline data gives a portable, self-contained file, but a leaked file leaks the key too. Protect it with file permissions.
  • Use straight quotes in shell commands. "Smart quotes" pasted from documents break them.

[!WARNING] Exam traps / remember:

  • certificate-authority takes a path. certificate-authority-data takes base64 content. Mixing them up is a classic broken-kubeconfig bug.
  • use-context changes the file, so it persists for later commands. Exam tasks often begin with kubectl config use-context <ctx>. Always run it.
  • Context cluster/user values must exactly match clusters[].name / users[].name.

[!NOTE] Beyond the course:

kubectl config get-contexts                                   # list contexts, * = current
kubectl config current-context
kubectl config set-context --current --namespace=finance      # change default ns of current context
kubectl config set-cluster prod --server=https://172.17.0.51:6443 --certificate-authority=/etc/kubernetes/pki/ca.crt --embed-certs=true
kubectl config set-credentials jane --client-certificate=jane.crt --client-key=jane.key --embed-certs=true
kubectl config set-context jane@prod --cluster=prod --user=jane --namespace=dev
kubectl config view --raw                                     # show real *-data values (not REDACTED)
kubectl config view --minify                                  # only the current context's entries
  • KUBECONFIG=file1:file2 merges several files. The first file wins for current-context.

7.13 Lab Solution KubeConfig

Source: KodeKloud notes

Theory: §7.12.

TaskCommand / answerGotcha
Where is the default kubeconfig?echo "$HOME"; ls -la "$HOME/.kube/config" → /root/.kube/config—
Clusters / users / contexts in default filekubectl config view (or cat ~/.kube/config) → 1 each: cluster kubernetes, user kubernetes-admin, context kubernetes-admin@kubernetes (also current)Count list entries under each key
Inspect /root/my-kube-configkubectl config view --kubeconfig=/root/my-kube-config → 4 clusters (production, development, kubernetes-on-aws, test-cluster-1), 4 contexts, current test-user@development—
User in context research?research → cluster test-cluster-1, user dev-user—
Cert file used by aws-user?users[aws-user].user.client-certificate → /etc/kubernetes/pki/users/aws-user/aws-user.crt—
Switch that file to researchkubectl --kubeconfig=/root/my-kube-config config use-context research then kubectl --kubeconfig=/root/my-kube-config config current-context → research--kubeconfig changes the named file, not ~/.kube/config
Use the file for the shell sessionexport KUBECONFIG=/root/my-kube-config; kubectl config current-contextLasts only for the shell
Make it the defaultunset KUBECONFIG first, then cp -a ~/.kube/config ~/.kube/config.backup then mv /root/my-kube-config ~/.kube/config → kubectl config current-context = researchBack up first. KUBECONFIG is the non-destructive alternative
kubectl get nodes fails with researchcompare users[dev-user] paths with ls -la /etc/kubernetes/pki/users/dev-user/ → files are dev-user.crt dev-user.csr dev-user.key, config says developer-user.crtPath mismatch: kubectl can't load the client cert
Fix the cert pathkubectl config set-credentials dev-user --client-certificate=/etc/kubernetes/pki/users/dev-user/dev-user.crt --client-key=/etc/kubernetes/pki/users/dev-user/dev-user.key (add --kubeconfig=/root/my-kube-config right after kubectl if the file was not moved)Or vi the file and fix developer-user.crt → dev-user.crt
Verifykubectl config view --minify (current context only) → kubectl get nodes works—

Relevant part of /root/my-kube-config:

apiVersion: v1
kind: Config
current-context: test-user@development
clusters:
- name: production
  cluster:
    certificate-authority: /etc/kubernetes/pki/ca.crt
    server: https://controlplane:6443
- name: development
  cluster:
    certificate-authority: /etc/kubernetes/pki/ca.crt
    server: https://controlplane:6443
- name: kubernetes-on-aws
  cluster:
    certificate-authority: /etc/kubernetes/pki/ca.crt
    server: https://controlplane:6443
- name: test-cluster-1
  cluster:
    certificate-authority: /etc/kubernetes/pki/ca.crt
    server: https://controlplane:6443
contexts:
- name: test-user@development
  context: {cluster: development, user: test-user}
- name: aws-user@kubernetes-on-aws
  context: {cluster: kubernetes-on-aws, user: aws-user}
- name: test-user@production
  context: {cluster: production, user: test-user}
- name: research
  context: {cluster: test-cluster-1, user: dev-user}
users:
- name: test-user
  user:
    client-certificate: /etc/kubernetes/pki/users/test-user/test-user.crt
    client-key: /etc/kubernetes/pki/users/test-user/test-user.key
- name: dev-user
  user:
    client-certificate: /etc/kubernetes/pki/users/dev-user/developer-user.crt   # BUG: file is dev-user.crt
    client-key: /etc/kubernetes/pki/users/dev-user/dev-user.key
- name: aws-user
  user:
    client-certificate: /etc/kubernetes/pki/users/aws-user/aws-user.crt
    client-key: /etc/kubernetes/pki/users/aws-user/aws-user.key

[!TIP] When kubectl auth fails, check the users: entry first: do the client-certificate / client-key paths exist? (ls them.) Then check whether the cert is valid for this cluster's CA.

7.14 API Groups

Source: KodeKloud notes

The API surface

  • Every interaction (kubectl, client libraries, curl) goes to kube-apiserver. Resources are organised by API group and version.
curl -k https://kube-master:6443/version        # {"major":"1","minor":...,"gitVersion":"v1.x.y",...}
curl -k https://kube-master:6443/api/v1/pods    # PodList from the core group
  • Top-level paths include /api, /apis, /healthz, /logs, /metrics, /openapi/v2, /version.

Core vs named groups

Core (legacy) groupNamed groups
URL/api/v1/<resource>/apis/<group>/<version>/<resource>
In YAML apiVersionv1<group>/<version>, e.g. apps/v1
In RBAC apiGroups"""apps", "networking.k8s.io", ...
Purposebasic primitivesnewer features, organised by area
  • Core resources: namespaces, pods, replicationcontrollers (rc), events, endpoints, nodes, bindings, persistentvolumes (PV), persistentvolumeclaims (PVC), configmaps, secrets, services.

Core API group: /api → /v1 → namespaces, pods, rc, events, endpoints, nodes, bindings, PV, PVC, configmaps, secrets, services

  • Named groups: apps, extensions, networking.k8s.io, storage.k8s.io, authentication.k8s.io, authorization.k8s.io, certificates.k8s.io, ...
GroupPathExample resources
core/api/v1pods, services, nodes, configmaps, secrets, persistentvolumes, persistentvolumeclaims
apps/apis/apps/v1deployments, replicasets, statefulsets
networking.k8s.io/apis/networking.k8s.io/v1networkpolicies
certificates.k8s.io/apis/certificates.k8s.io/v1certificatesigningrequests
authorization.k8s.io/apis/authorization.k8s.io/v1SubjectAccessReview
  • Hierarchy: API group → version → resource → verbs. Verbs (actions) include list, get, create, delete, update, watch. Authorization rules are written against exactly these (group, resource, verb). See §7.16.

Named API groups under /apis: apps/v1 → deployments, replicasets, statefulsets with verbs list/get/create/delete/update/watch; networking.k8s.io/v1 → networkpolicies; certificates.k8s.io/v1 → certificatesigningrequests

Discovery

curl -k https://localhost:6443          # lists available paths (if authorised)
curl -k https://localhost:6443/apis     # named groups and their versions
  • Without credentials most paths return 403 Forbidden, User "system:anonymous" cannot get path "/".

kubectl proxy

kubectl proxy                            # Starting to serve on 127.0.0.1:8001
curl http://localhost:8001               # no certs needed: proxy uses your kubeconfig credentials
curl http://localhost:8001/api/v1/namespaces/default/pods
kube-proxykubectl proxy
Whatcluster component on every nodeclient-side local HTTP proxy
Doesprograms node network rules for Service → Pod trafficforwards local requests to kube-apiserver using kubeconfig creds
Port—127.0.0.1:8001 default

[!WARNING] Exam traps / remember:

  • Core group is the empty string "" in RBAC rules and plain v1 in apiVersion. Deployments are in apps, not core.
  • Don't confuse kubectl proxy (local API access, port 8001) with kube-proxy (Service networking).

[!NOTE] Beyond the course:

kubectl api-resources                      # NAME, SHORTNAMES, APIVERSION, NAMESPACED, KIND
kubectl api-resources --api-group=apps     # resources of one group ("" = core)
kubectl api-resources --namespaced=false   # cluster-scoped resources
kubectl api-versions                       # all group/version pairs
kubectl get --raw /apis | head             # raw API call with kubeconfig creds, no proxy needed
kubectl explain deployment                 # shows GROUP/VERSION of a kind
  • extensions (shown on the slide) no longer serves any resources: Deployments/DaemonSets/ReplicaSets moved to apps/v1, Ingress to networking.k8s.io/v1 (removed in v1.16 / v1.22).
  • metadata.selfLink in the sample output was removed in v1.24.

Discovery paths returned: /api, /api/v1, /apis, /apis/, /healthz, /logs, /metrics, /openapi/v2, /swagger-2.0.0.json. /version is a separate endpoint.

7.15 Authorization

Source: KodeKloud notes

Purpose

  • Authentication = who you are (§7.3). Authorization = what an authenticated user or service account may do.
  • Goals: developers can deploy apps but not touch nodes, networking or storage config. CI/CD (e.g. Jenkins) and monitoring get the minimum needed. Users are restricted to their namespaces.

Modes

ModeScope / useNotes
Nodekubelet → API server requestsBuilt-in; scopes each kubelet to what its node needs
ABACper-user/group attribute policiesJSON policy file; every change = edit + API server restart. Hard to manage
RBACrecommended for most clustersRoles/ClusterRoles + bindings; declarative, edit a role and all bound subjects follow
Webhookdelegate the decision to an external service (e.g. Open Policy Agent)API server POSTs request details, enforces the allow/deny answer
AlwaysAllowallow everythingTesting/bootstrap only; insecure
AlwaysDenydeny everythingTesting only
  • In production prefer fine-grained modes (RBAC, plus Webhook if needed) over global permissive ones.

Node authorizer

  • Kubelets authenticate with certs whose CN is system:node:<nodeName> and O is system:nodes (the system: prefix and system:nodes group are what the Node authorizer looks for).
  • It grants kubelets only what they need:
    • Read: services, endpoints, nodes, pods
    • Write: node status, pod status, events
  • It applies only to node-originated requests.

Node Authorizer: kubelet with certificate system:node:node01 in group SYSTEM:NODES; reads services/endpoints/nodes/pods, writes node status/pod status/events

ABAC policy file

One JSON object per line, one line per user/group grant:

{"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "spec": {"user": "dev-user", "namespace": "*", "resource": "pods", "apiGroup": "*"}}
{"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "spec": {"user": "dev-user-2", "namespace": "*", "resource": "pods", "apiGroup": "*"}}
{"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "spec": {"group": "dev-users", "namespace": "*", "resource": "pods", "apiGroup": "*"}}
{"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "spec": {"user": "security-1", "namespace": "*", "resource": "certificatesigningrequests", "apiGroup": "*"}}

Enabled with --authorization-mode=ABAC --authorization-policy-file=<file>.

[!CAUTION] Correction: the course's ABAC lines omit apiVersion and use "resource": "csr". Real policy lines need "apiVersion": "abac.authorization.kubernetes.io/v1beta1", and resource must be the plural resource name (certificatesigningrequests), not a kubectl short name. Corrected above.

Multiple modes: evaluation order

  • Set on kube-apiserver with --authorization-mode, a comma-separated list. Default if unset: AlwaysAllow.
  • Authorizers are consulted in the listed order. Each returns Allow (stop, permit), Deny (stop, reject) or No opinion (pass to the next).
  • Example --authorization-mode=Node,RBAC,Webhook: Node handles kubelet requests first. Otherwise RBAC is tried, then Webhook. The first Allow/Deny wins.

Authorization chain: Node then RBAC then Webhook; Allow or Deny stops evaluation, no opinion passes to the next Diagram (new): short-circuit evaluation of a comma-separated --authorization-mode list.

Configuring (systemd-managed API server)

# kube-apiserver.service (excerpt) — insecure default shown
ExecStart=/usr/local/bin/kube-apiserver \
  --advertise-address=${INTERNAL_IP} \
  --allow-privileged=true \
  --apiserver-count=3 \
  --authorization-mode=AlwaysAllow \
  --bind-address=0.0.0.0 \
  --enable-swagger-ui=true \
  --etcd-cafile=/var/lib/kubernetes/ca.pem \
  --etcd-certfile=/var/lib/kubernetes/apiserver-etcd-client.crt \
  --etcd-keyfile=/var/lib/kubernetes/apiserver-etcd-client.key \
  --etcd-servers=https://127.0.0.1:2379 \
  --event-ttl=1h \
  --kubelet-certificate-authority=/var/lib/kubernetes/ca.pem \
  --kubelet-client-certificate=/var/lib/kubernetes/apiserver-kubelet-client.crt \
  --kubelet-client-key=/var/lib/kubernetes/apiserver-kubelet-client.key \
  --service-node-port-range=30000-32767 \
  --client-ca-file=/var/lib/kubernetes/ca.pem \
  --tls-cert-file=/var/lib/kubernetes/apiserver.crt \
  --tls-private-key-file=/var/lib/kubernetes/apiserver.key \
  --v=2

Change to --authorization-mode=Node,RBAC,Webhook, then restart the API server (systemctl daemon-reload && systemctl restart kube-apiserver).

[!WARNING] Exam traps / remember:

  • Know all six modes: Node, ABAC, RBAC, Webhook, AlwaysAllow, AlwaysDeny.
  • Order matters: the first authorizer that returns Allow or Deny decides.
  • To find the modes on a kubeadm cluster: grep authorization-mode /etc/kubernetes/manifests/kube-apiserver.yaml or ps -ef | grep kube-apiserver → typically Node,RBAC (§7.17).

[!NOTE] Beyond the course:

  • If every authorizer has no opinion, the request is denied. RBAC never returns Deny, only Allow or no opinion, so you cannot write "deny" rules in RBAC.
  • On kubeadm clusters, edit /etc/kubernetes/manifests/kube-apiserver.yaml. The kubelet restarts the static pod automatically, so no systemctl is needed.
  • --enable-swagger-ui has been removed from current kube-apiserver. Newer clusters can use --authorization-config (structured authorization config file, GA in v1.32) instead of --authorization-mode. The two flags are mutually exclusive.
  • Webhook: use it for policy managed centrally outside the cluster, or when policies are complex or change often (e.g. OPA).
  • RBAC also suits CI/CD and service-account-based automation.

7.16 Role Based Access Controls

Source: KodeKloud notes

Role = namespaced set of rules

Rule fieldMeaningExample
apiGroupsAPI group(s); "" = core[""]
resourcesresource types (plural)["pods"]
verbsallowed actions["get","create"]
resourceNamesoptional: only these named objects["blue","orange"]
# developer-role.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: developer
  namespace: default            # Role lives in (and grants within) this namespace
rules:
- apiGroups: [""]               # core API group
  resources: ["pods"]
  verbs: ["get", "list", "create", "delete"]
- apiGroups: [""]
  resources: ["configmaps"]
  verbs: ["create"]
kubectl apply -f developer-role.yaml     # or: kubectl create -f developer-role.yaml

RoleBinding = attach Role to subjects

# devuser-developer-binding.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: devuser-developer-binding
  namespace: default                    # same namespace as the Role
subjects:                               # who: User, Group or ServiceAccount
- kind: User
  name: dev-user
  apiGroup: rbac.authorization.k8s.io
roleRef:                                # what: exactly one Role (or ClusterRole)
  kind: Role
  name: developer
  apiGroup: rbac.authorization.k8s.io
kubectl apply -f devuser-developer-binding.yaml   # or kubectl create -f ...
  • Roles and RoleBindings are namespace-scoped. The RoleBinding sits in the Role's namespace (it may instead reference a ClusterRole). For cluster-wide access use ClusterRole + ClusterRoleBinding (§7.18).

RBAC: Role holds rules, RoleBinding references the Role via roleRef and lists subjects (User, Group, ServiceAccount) Diagram (new): how Role, RoleBinding and subjects relate inside a namespace.

View RBAC objects

CommandShows
kubectl get rolesRoles in the current namespace
kubectl get rolebindingsRoleBindings in the current namespace
kubectl describe role developerPolicyRule table: Resources, Non-Resource URLs, Resource Names, Verbs
kubectl describe rolebinding devuser-developer-bindingRole (Kind/Name) + Subjects (Kind, Name, Namespace)
$ kubectl describe role developer
PolicyRule:
  Resources   Non-Resource URLs  Resource Names  Verbs
  ---------   -----------------  --------------  -----
  configmaps  []                 []              [create]
  pods        []                 []              [get list create delete]

(The course output also shows watch for pods. That only appears if watch is in the Role's verbs.)

Test permissions: kubectl auth can-i

kubectl auth can-i create deployments                     # yes/no for the current user
kubectl auth can-i delete nodes                           # no
kubectl auth can-i create deployments --as=dev-user       # impersonate a user (admin only)
kubectl auth can-i create pods --as=dev-user -n test      # namespace-specific check -> no if only granted in default

Restrict to named objects

rules:
- apiGroups: [""]
  resources: ["pods"]
  resourceNames: ["blue", "orange"]   # only these two pods; all other pods denied
  verbs: ["get"]

[!WARNING] Exam traps / remember:

  • apiGroups: [""] for core resources. Deployments need apiGroups: ["apps"].
  • Resources are plural and lowercase (pods, deployments).
  • Subject kind is case-sensitive: User, Group, ServiceAccount. Users/Groups use apiGroup: rbac.authorization.k8s.io. A ServiceAccount subject needs namespace (and apiGroup: "").
  • Role/RoleBinding work only in their namespace. can-i without -n checks the current namespace.

[!NOTE] Beyond the course: imperative forms (much faster in the exam):

kubectl create role developer --verb=get,list,create,delete --resource=pods -n default
kubectl create role cm-reader --verb=get --resource=configmaps --resource-name=kube-proxy -n kube-system
kubectl create rolebinding devuser-developer-binding --role=developer --user=dev-user -n default
kubectl create rolebinding sa-bind --role=developer --serviceaccount=default:my-sa   # ns:name
kubectl create rolebinding grp-bind --role=developer --group=dev-users
kubectl auth can-i --list --as=dev-user -n default        # everything dev-user may do
kubectl auth can-i get pods --as=system:serviceaccount:default:my-sa
  • Standard resource verbs: get, list, watch, create, update, patch, delete, deletecollection (* = all). Special permissions include bind, escalate, impersonate, and CSR signer approve / sign.
  • roleRef is immutable. To point a binding at another role, delete and recreate it.

7.17 Lab Role Based Access Controls

Source: KodeKloud notes

Theory: §7.15 (modes), §7.16 (Role/RoleBinding).

TaskCommand / answerGotcha
Authorization modes enabled?grep authorization-mode /etc/kubernetes/manifests/kube-apiserver.yaml or ps aux | grep kube-apiserver → --authorization-mode=Node,RBACRBAC must be enabled for Role/ClusterRole/bindings to be enforced
How many Roles (all namespaces)?kubectl get roles -A --no-headers | wc -l → 12--no-headers so the header line isn't counted
Resources the kube-proxy Role can accesskubectl describe role kube-proxy -n kube-system → configmaps, Resource Names [kube-proxy], Verbs [get]Can only get that one ConfigMap: no update/delete
Who is kube-proxy Role bound to?kubectl get rolebindings -n kube-system, then kubectl describe rolebinding kube-proxy -n kube-system → Group system:bootstrappers:kubeadm:default-node-token—
Is dev-user in kubeconfig?kubectl config view → users: has dev-user (and kubernetes-admin)—
Can dev-user list pods in default?kubectl get pods --as=dev-user → Forbidden: User "dev-user" cannot list resource "pods" in API group "" in the namespace "default"—
Allow dev-user to list/create/delete pods in defaultkubectl create role developer --verb=list,create,delete --resource=pods + kubectl create rolebinding dev-user-binding --role=developer --user=dev-userCheck with kubectl describe role developer / describe rolebinding dev-user-binding
Why can't dev-user get dark-blue-app in blue?kubectl --as=dev-user get pod dark-blue-app -n blue → Forbidden. Discover bindings/roles: kubectl get roles -n blue; kubectl get rolebindings -n blue; then kubectl describe role developer -n blue → pods, Resource Names [blue-app], verbs [get watch create delete]resourceNames restricts the rule to blue-app only
Fix itkubectl edit role developer -n blue → change resourceNames to dark-blue-app (or remove resourceNames) → re-run the get as dev-user—
Let dev-user create Deployments in blueFirst attempt fails: deployments.apps is forbidden ... in API group "apps". kubectl edit role developer -n blue → add an apps/deployments rule → kubectl --as=dev-user create deployment nginx --image=nginx -n blue → deployment.apps/nginx createdPlural deployments, group apps (describe shows deployments.apps)

Final Role in blue:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: developer
  namespace: blue
rules:
- apiGroups: [""]
  resources: ["pods"]
  resourceNames: ["dark-blue-app"]     # only this pod
  verbs: ["get", "watch", "create", "delete"]
- apiGroups: ["apps"]                  # Deployments are NOT in the core group
  resources: ["deployments"]
  verbs: ["get", "watch", "create", "delete"]
$ kubectl describe role developer -n blue
  Resources         Non-Resource URLs  Resource Names   Verbs
  pods              []                 [dark-blue-app]  [get watch create delete]
  deployments.apps  []                 []               [get watch create delete]
  • Omitting resourceNames = rule applies to all objects of that type in the namespace.
  • The error message tells you exactly what's missing: verb, resource, API group and namespace. Turn those straight into a rule.

[!CAUTION] Correction: the lab says that with resourceNames, create still works if the new object's name matches the list. In Kubernetes, create (and deletecollection) cannot be restricted by resourceNames: the name isn't checked at authorization time, so a rule with resourceNames never grants create. To allow creating pods, put create in a separate rule without resourceNames.

[!NOTE] Beyond the course: the lab's manifest shows image k8s.gcr.io/kube-apiserver:v1.23.0. Current clusters pull from registry.k8s.io.

# /etc/kubernetes/manifests/kube-apiserver.yaml (excerpt)
    - --authorization-mode=Node,RBAC
    - --client-ca-file=/etc/kubernetes/pki/ca.crt
    - --enable-admission-plugins=NodeRestriction
    - --enable-bootstrap-token-auth=true

7.18 Cluster Roles

Source: KodeKloud notes

Namespaced vs cluster-scoped resources

  • Role + RoleBinding (§7.16) are namespaced: created in a namespace (e.g. default), grant access only inside it.
  • Cluster-scoped resources belong to no namespace — you can't say "node01 is in namespace dev". Examples: nodes, PersistentVolumes.
  • Namespaced (normally managed with Roles): pods, replicasets, jobs, deployments, services, secrets, roles, rolebindings, configmaps, PVCs.
  • Cluster-scoped: nodes, PVs, clusterroles, clusterrolebindings, certificatesigningrequests, namespaces.
  • ClusterRoles and ClusterRoleBindings are themselves cluster-scoped objects.

Namespaced resources (pods, replicasets, jobs, deployments, services, secrets, roles, rolebindings, configmaps, PVC) vs cluster-scoped (nodes, PV, clusterroles, clusterrolebindings, CSRs, namespaces)

kubectl api-resources -o wide                 # NAMESPACED column = true/false
kubectl api-resources --namespaced=false      # only cluster-scoped kinds (Beyond the course: filter flag)
kubectl api-resources --namespaced=true       # only namespaced kinds

ClusterRole + ClusterRoleBinding

  • ClusterRole = same structure as a Role (rules: apiGroups / resources / verbs), but effect is cluster-wide.
  • Course examples: Cluster Admin → view/create/delete nodes; Storage Admin → view/create PVs, delete PVCs.
  • ClusterRoleBinding links subjects (User / Group / ServiceAccount) to a ClusterRole. Both can go in one file separated by ---.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: cluster-administrator          # no namespace: cluster-scoped
rules:
- apiGroups: [""]                      # "" = core API group (nodes, pods, PVs...)
  resources: ["nodes"]
  verbs: ["list", "get", "create", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: cluster-admin-role-binding
subjects:
- kind: User
  name: cluster-admin
  apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: ClusterRole
  name: cluster-administrator
  apiGroup: rbac.authorization.k8s.io
kubectl apply -f cluster-admin-rolebinding.yaml

ClusterRoles on namespaced resources

  • A ClusterRole is not limited to cluster-scoped resources; it can list namespaced ones (e.g. pods).
  • Bound via ClusterRoleBinding → the subject gets that access in every namespace (vs a Role = one namespace only).
  • Least privilege: be deliberate — a ClusterRole on pods + ClusterRoleBinding = pods everywhere.
  • Kubernetes creates many default ClusterRoles at cluster setup (e.g. cluster-admin, system:node, plus admin/edit/view — see §7.19). ClusterRoleBindings can reference them; know them when designing RBAC for operators and ServiceAccounts.

[!CAUTION] Correction: the original lists system:masters as a built-in ClusterRole. It is a group: the default cluster-admin ClusterRoleBinding binds the cluster-admin ClusterRole to the group system:masters (see §7.19).

RBAC objectScopeUse caseExample
RoleNamespacedPermissions inside one namespacekind: Role, resources: ["pods"]
RoleBindingNamespacedBind a Role to subjects in that namespacekind: RoleBinding → Role
ClusterRoleCluster-wide (can also reference namespaced resources)Cluster-scoped resources, or a namespaced resource across all namespaceskind: ClusterRole, resources: ["nodes"] or ["pods"]
ClusterRoleBindingCluster-wideBind a ClusterRole to subjects cluster-wide / all namespaceskind: ClusterRoleBinding → ClusterRole

[!NOTE] Beyond the course: a RoleBinding may reference a ClusterRole. The rules then apply only inside the RoleBinding's namespace. This is the usual way to reuse one definition (e.g. the built-in edit or view) in many namespaces. Valid combinations:

BindingroleRefEffective scope
RoleBindingRole (same ns)that namespace
RoleBindingClusterRolethat namespace only
ClusterRoleBindingClusterRoleall namespaces + cluster-scoped resources
ClusterRoleBindingRolenot allowed

roleRef is immutable: to change the referenced role, delete and recreate the binding.

[!WARNING] Exam traps / remember:

  • Nodes, PVs, StorageClasses, namespaces and CSRs need a ClusterRole. A Role (even in default) cannot grant them.
  • ClusterRole/ClusterRoleBinding manifests have no metadata.namespace.
  • ClusterRoleBinding to a ClusterRole that covers pods = pods in all namespaces.

7.19 Lab Solution Cluster Roles

Source: KodeKloud notes

Theory: §7.18 (ClusterRoles), §7.16 (RBAC).

TaskCommand / techniqueNotes
Count ClusterRoleskubectl get clusterroles --no-headers | wc -llab: 69 (lots of system:* and controller roles, plus view, edit, admin, traefik)
Count ClusterRoleBindingskubectl get clusterrolebindings --no-headers | wc -llab: 54
Bindings that use cluster-adminkubectl get clusterrolebindings | grep cluster-adminROLE column shows ClusterRole/cluster-admin (lab: cluster-admin, helm-kube-system-traefik, helm-kube-system-traefik-crd)
Who is bound to cluster-admin?kubectl describe clusterrolebinding cluster-adminSubjects: Group system:masters. Labels kubernetes.io/bootstrapping=rbac-defaults, annotation rbac.authorization.kubernetes.io/autoupdate: true
What does cluster-admin allow?kubectl describe clusterrole cluster-admin*.* resources with verbs [*], plus non-resource URLs [*] with [*] → everything
Default ClusterRolePurpose
cluster-adminFull access: all resources, all verbs
viewRead-only on most resources
editRead/write on namespaced resources (not cluster-scoped)
adminAdministrative access within a namespace

[!NOTE] Beyond the course: view cannot read Secrets. edit cannot view or modify Roles/RoleBindings. admin can manage Roles/RoleBindings in the namespace but not the namespace itself or its ResourceQuota. admin, edit and view are meant to be granted per namespace with a RoleBinding.

Drill: give user michelle node access

kubectl get nodes --as michelle
# Forbidden: User "michelle" cannot list resource "nodes" in API group "" at the cluster scope

kubectl create clusterrole michelle-role --verb=get,list,watch --resource=nodes
kubectl create clusterrolebinding michelle-role-binding --clusterrole=michelle-role --user=michelle

kubectl describe clusterrole michelle-role             # nodes  [get list watch]
kubectl describe clusterrolebinding michelle-role-binding   # Role: ClusterRole/michelle-role, Subject: User michelle
kubectl get nodes --as michelle                         # now succeeds

Drill: give michelle storage access (PVs + StorageClasses)

kubectl api-resources            # find exact names + API groups:
# persistentvolumes   pv  v1                 false  PersistentVolume
# persistentvolumeclaims pvc v1             true   PersistentVolumeClaim
# storageclasses      sc  storage.k8s.io/v1  false  StorageClass

kubectl create clusterrole storage-admin \
  --resource=persistentvolumes,storageclasses --verb=list,create,get,watch
kubectl get clusterrole storage-admin -o yaml   # verify the generated rules
kubectl create clusterrolebinding michelle-storage-binding --clusterrole=storage-admin --user=michelle
kubectl get storageclasses --as michelle
  • kubectl create clusterrole automatically splits the rules by API group: persistentvolumes goes under apiGroups: [""], storageclasses under apiGroups: ["storage.k8s.io"]. If you write YAML by hand, you must set the groups yourself.
# generated rules (trimmed)
rules:
- apiGroups: [""]
  resources: ["persistentvolumes"]
  verbs: ["list", "create", "get", "watch"]
- apiGroups: ["storage.k8s.io"]
  resources: ["storageclasses"]
  verbs: ["list", "create", "get", "watch"]

[!TIP] Verify with --as <user> (impersonation). Beyond the course: kubectl auth can-i list nodes --as michelle answers yes/no without running the real command. kubectl auth can-i --list --as michelle lists everything the user may do.

[!WARNING] Exam traps / remember:

  • Do not bind cluster-admin when a small custom ClusterRole will do.
  • Resource names in --resource are plural lowercase (persistentvolumes, storageclasses). Short names (pv, sc) are also accepted, but check the generated YAML.
  • ClusterRoleBinding subject flags: --user=, --group=, --serviceaccount=<ns>:<name>.

7.20 Service Accounts

Source: KodeKloud notes

Account types

TypeForExamples
User accountHumansadmin, developer
Service accountMachines/apps calling the APIPrometheus (scrapes API metrics), Jenkins (creates/modifies resources), custom dashboard
  • Scenario: Python app my-kubernetes-dashboard lists Pods through the API. Running in-cluster, it should authenticate with a ServiceAccount.
  • ServiceAccount = namespaced identity object ("ID card"). Its token is presented as a Bearer token ("barcode" that is scanned to check the ID).

ServiceAccount token flow Diagram (new): the kubelet mounts a time-bound SA token into the Pod. The app or an external tool sends it as a Bearer token, and the API server authenticates and then checks RBAC.

Default SA and token mounting

  • Every namespace automatically has a default ServiceAccount. A Pod without a ServiceAccount set uses default.
  • The token is mounted through a projected volume (kube-api-access-xxxxx) at /var/run/secrets/kubernetes.io/serviceaccount.
File in mountPurpose
tokenSA Bearer token for API authentication
namespaceNamespace the Pod runs in
ca.crtCluster CA certificate, used to verify the API server's TLS certificate
kubectl get serviceaccount                         # lists SAs in current ns (default always present)
kubectl describe pod my-kubernetes-dashboard       # "Service Account: default"; Mounts + Volumes below
kubectl exec -it my-kubernetes-dashboard -- ls /var/run/secrets/kubernetes.io/serviceaccount
# ca.crt  namespace  token
# describe pod excerpt
Volumes:
  kube-api-access-rtvkd:
    Type:                    Projected
    TokenExpirationSeconds:  3607
    ConfigMapName:           kube-root-ca.crt
    DownwardAPI:             true

Create a custom SA and attach it

  • The default SA has minimal privileges. Create a dedicated SA and give it only the access it needs with RBAC: Role/ClusterRole + RoleBinding/ClusterRoleBinding (§7.16, §7.18).
kubectl create serviceaccount dashboard-sa      # imperative
kubectl describe serviceaccount dashboard-sa    # Image pull secrets / Mountable secrets / Tokens: <none>
apiVersion: v1
kind: ServiceAccount
metadata:
  name: dashboard-sa
  namespace: default
---
apiVersion: v1
kind: Pod
metadata:
  name: my-kubernetes-dashboard
spec:
  serviceAccountName: dashboard-sa     # pod-level field; omitted → "default"
  containers:
  - name: my-kubernetes-dashboard
    image: my-kubernetes-dashboard
  • Check: kubectl describe pod my-kubernetes-dashboard shows Service Account: dashboard-sa. The token is mounted at the same path.

Disable auto-mounting: automountServiceAccountToken: false

apiVersion: v1
kind: ServiceAccount
metadata:
  name: dashboard-sa
  namespace: default
automountServiceAccountToken: false    # top-level field on the SA (not under spec)
---
apiVersion: v1
kind: Pod
metadata:
  name: my-kubernetes-dashboard
spec:
  automountServiceAccountToken: false  # per-Pod; overrides the SA setting
  serviceAccountName: dashboard-sa
  containers:
  - name: my-kubernetes-dashboard
    image: my-kubernetes-dashboard
  • Set it to false on the Pod, or on the SA (unless the Pod overrides it), and no token is mounted.

Tokens for external use

kubectl create token dashboard-sa                  # prints a short-lived JWT (default ~1h)
kubectl create token dashboard-sa --duration=2h    # request longer validity

# inspect JWT header + payload
jq -R 'split(".") | .[0], .[1] | @base64d | fromjson' <<< 'eyJhbGciOiJSUz...'
#   payload: "sub": "system:serviceaccount:default:dashboard-sa", "aud", "exp", "iat", "iss", "nbf",
#            "kubernetes.io": {namespace, serviceaccount{name, uid}}

curl --insecure --header "Authorization: Bearer eyJhbGciOiJSUz..." https://192.168.56.70:6443/api
ActionCommand
Create SAkubectl create serviceaccount dashboard-sa or apply kind: ServiceAccount YAML
Token for SAkubectl create token dashboard-sa [--duration=2h]
Use SA in Podspec.serviceAccountName: dashboard-sa
Stop token mountautomountServiceAccountToken: false on the SA or Pod

[!NOTE] Beyond the course (v1.24+ behaviour): token Secrets are no longer created automatically for SAs. On new clusters the SECRETS column shows 0 (the course output shows 1 for an old default SA). Pods get projected, time-bound tokens that the kubelet refreshes. If you really need a long-lived token, create a Secret by hand:

apiVersion: v1
kind: Secret
metadata:
  name: dashboard-sa-token
  annotations:
    kubernetes.io/service-account.name: dashboard-sa   # token controller fills in token/ca.crt
type: kubernetes.io/service-account-token

[!WARNING] Exam traps / remember:

  • The username of an SA in RBAC errors and JWT sub is system:serviceaccount:<namespace>:<sa-name>.
  • RoleBinding subject for an SA: kind: ServiceAccount, name, namespace (no apiGroup). Imperative form: --serviceaccount=<ns>:<name>.
  • The field is serviceAccountName. serviceAccount is the deprecated alias you may still see in -o yaml output.
  • Beyond the course: you cannot change serviceAccountName of an existing Pod. Delete and recreate it, or edit the Deployment, which rolls out new Pods.
  • Mount path to memorise: /var/run/secrets/kubernetes.io/serviceaccount.

7.21 Lab Solution Service Accounts

Source: KodeKloud notes

Theory: §7.20. Scenario: the web-dashboard Deployment cannot list pods; fix it with a dedicated SA and RBAC.

#TaskCommand / technique → answer
1Count SAs in defaultkubectl get serviceaccounts -n default → 2 (default, dev)
2Secret token of default SAkubectl describe serviceaccount default -n default → Tokens: <none> (no legacy Secret; modern clusters use projected tokens)
3Image of dashboard Deploymentkubectl get deployments then kubectl describe deployment web-dashboard → gcr.io/kodekloud/customimage/my-kubernetes-dashboard (port 8080/TCP, env PYTHONUNBUFFERED=1)
4Which account does the app use?UI error pods is forbidden: User "system:serviceaccount:default:default" cannot list resource "pods" in API group "" in the namespace "default" → the default SA. You can also check serviceAccountName in the Pod/Deployment spec
5Where are credentials mounted?kubectl get pods -l name=web-dashboard, then kubectl describe pod <pod> → Service Account: default, mount /var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-xxxxx (ro), projected volume (TokenExpirationSeconds: 3607, ConfigMapName: kube-root-ca.crt, DownwardAPI: true)
6Create SA dashboard-sakubectl create serviceaccount dashboard-sa -n default. RBAC manifests are provided in /var/rbac/: pod-reader-role.yaml, dashboard-sa-role-binding.yaml (apply them if they are not already present)
7Token for the UIkubectl create token dashboard-sa -n default → paste into the dashboard token field and click "Load Dashboard"; pods are now listed
8Make the Deployment use the SASet spec.template.spec.serviceAccountName: dashboard-sa (see below)
9Verifykubectl get deployment web-dashboard, kubectl get pods -l name=web-dashboard, kubectl describe pod <new-pod> → Service Account: dashboard-sa + token mount; the UI no longer needs a pasted token
# Step 6: if you must build the RBAC yourself (Beyond the course: imperative equivalent of /var/rbac files)
kubectl create role pod-reader --verb=get,watch,list --resource=pods -n default
kubectl create rolebinding read-pods --role=pod-reader --serviceaccount=default:dashboard-sa -n default
kubectl auth can-i list pods --as=system:serviceaccount:default:dashboard-sa -n default   # yes

# Step 8: course method
kubectl get deployment web-dashboard -n default -o yaml > dashboard.yaml
# edit: add serviceAccountName under spec.template.spec
kubectl apply -f dashboard.yaml
spec:
  template:
    spec:
      serviceAccountName: dashboard-sa   # pod template spec, NOT the Deployment's top-level spec

[!NOTE] Beyond the course: to set it in one command, kubectl set serviceaccount deployment web-dashboard dashboard-sa. Or use kubectl edit deploy web-dashboard. Both trigger a rollout.

[!WARNING] Exam traps / remember:

  • serviceAccountName goes under spec.template.spec. At the Deployment's top-level spec it is wrong.
  • kubectl apply on a Deployment originally made with kubectl create warns about a missing last-applied-configuration annotation. This is harmless.
  • A pasted token only tests the permissions. The real fix is the Deployment SA, so new Pods mount the token automatically.

7.22 Image Security

Source: KodeKloud notes

Image name format

[registry-host[:port]/][namespace/]repository[:tag | @sha256:<digest>]

PartMeaningDefault / example
registry-host[:port]Registry to pull fromdefault docker.io; gcr.io, private-registry.io:5000
namespaceUser/account/orgdefault library (official images); myorg
repositoryImage namenginx, myapp
tag / digestVersiondefault :latest; :1.19, @sha256:<digest>
  • nginx = docker.io/library/nginx:latest
  • myorg/webapp:1.0 = docker.io/myorg/webapp:1.0
  • gcr.io/my-project/my-image:stable = pulled from Google Container Registry.
  • If you leave parts out, the defaults apply (Docker Hub + library + latest). Writing full names makes pulls reproducible and safer.
RegistryHostNotes
Docker Hubdocker.ioPublic; official images in library
Google Container Registrygcr.ioKubernetes test images (gcr.io/kubernetes-e2e-test-images/...)
Amazon ECR / Azure Container Registry–Managed private registries
Harbor–Self-hosted
  • Use a private registry with access control for proprietary or internal images.

[!NOTE] Beyond the course: Google Container Registry has been replaced by Artifact Registry (*-docker.pkg.dev). Kubernetes images now come from registry.k8s.io (the old k8s.gcr.io is frozen).

Private registry: Docker vs Kubernetes

# Docker (local): credentials are stored in the local Docker config
docker login private-registry.io
docker run private-registry.io/apps/internal-app:latest
  • In Kubernetes, the kubelet / container runtime on the node pulls the image, so it needs the credentials. Store them in a Secret of type kubernetes.io/dockerconfigjson and reference it in the Pod with imagePullSecrets.
kubectl create secret docker-registry regcred \
  --docker-server=private-registry.io \
  --docker-username=registry-user \
  --docker-password=registry-password \
  --docker-email=registry-user@org.com
apiVersion: v1
kind: Pod
metadata:
  name: nginx-pod
spec:
  containers:
  - name: nginx
    image: private-registry.io/apps/internal-app:latest
  imagePullSecrets:          # Pod-level (sibling of containers), list of Secret names
  - name: regcred

Best practices

  • Use fully qualified names (registry + namespace + tag or digest), so nothing is pulled by accident from a public registry.
  • In production, pin images by digest (@sha256:...) so the content cannot change.
  • Restrict registry access with strong credentials or IAM (AWS IAM for ECR, GCP IAM for GCR).
  • Scan images for vulnerabilities in CI/CD before deploying.
  • Keep pull Secrets in the namespace that uses them, with least privilege.

[!NOTE] Beyond the course: add imagePullSecrets to a ServiceAccount and every Pod created afterwards using that SA inherits it (recreate existing Pods, e.g. kubectl rollout restart deploy/<name>): kubectl patch serviceaccount default -p '{"imagePullSecrets":[{"name":"regcred"}]}'

[!WARNING] Exam traps / remember:

  • The Secret type in the command is docker-registry (the object's type becomes kubernetes.io/dockerconfigjson). An Opaque Secret will not work for pulls. generic --type=kubernetes.io/dockerconfigjson --from-file=.dockerconfigjson=<config.json> also creates a correctly typed pull Secret.
  • The Secret must be in the same namespace as the Pod.
  • imagePullSecrets goes under the Pod spec, not inside a container.

7.23 Lab Solution Image Security

Source: KodeKloud notes

Theory: §7.22. Steps: point Deployment web at the private registry, see the pull fail, create the Secret, add imagePullSecrets.

#TaskCommand / technique → result
QSecret type for registry credentials?docker-registry (stores server, username, password/token, email)
1Current imagekubectl get deploy, then kubectl describe deploy web → nginx:alpine (container nginx, imagePullPolicy: IfNotPresent). No registry given = Docker Hub
2Use private registry imagekubectl edit deploy web → image: myprivateregistry.com:5000/nginx:alpine. This starts a rollout with a new ReplicaSet; the new Pod goes to ImagePullBackOff (events: unauthorized / failed pull) while the old Pods keep running
3Create the Secretsee below; name private-reg-cred in the current namespace
4Reference itadd imagePullSecrets under .spec.template.spec. This triggers another rollout and the new Pods reach Running
5Watchkubectl rollout status deploy/web; kubectl get pods --watch (old Pods go Terminating)
kubectl set image deploy/web nginx=myprivateregistry.com:5000/nginx:alpine   # Beyond the course: alternative to edit

kubectl create secret docker-registry private-reg-cred \
  --docker-server=myprivateregistry.com:5000 \
  --docker-username=dock_user \
  --docker-password=dock_password \
  --docker-email=dock_user@myprivateregistry.com
# kubectl edit deploy web
spec:
  template:
    spec:
      imagePullSecrets:              # pod spec level
      - name: private-reg-cred       # must match the Secret name exactly
      containers:
      - name: nginx
        image: myprivateregistry.com:5000/nginx:alpine
        imagePullPolicy: IfNotPresent

[!WARNING] Exam traps / remember:

  • Put the registry host:port both in the image name and in --docker-server (myprivateregistry.com:5000).
  • The path for Deployments is .spec.template.spec.imagePullSecrets. It is not under containers[].
  • Secrets are namespaced, so create the Secret in the Deployment's namespace.
  • Never commit registry Secrets or credentials to Git in plaintext. Use a secrets manager, encrypted manifests or a CI/CD secret store.

7.24 Prerequisite Security in Docker

Source: KodeKloud notes

Isolation with namespaces

  • Containers share the host kernel; there is no guest kernel. Isolation comes from Linux namespaces: PID, mount, network, IPC, UTS. Docker normally creates these per container; Kubernetes shares some at Pod scope (e.g. network). User namespaces are opt-in.
  • A container process is a real host process. It has a different PID in the host's PID namespace.
docker run -d --name sleeper ubuntu sleep 3600
docker exec -it sleeper ps aux     # inside: sleep 3600 is PID 1, user root
ps aux | grep sleep                # on host: same process, e.g. PID 3816, user root

Users inside containers

  • An image has its own /etc/passwd and /etc/group, but by default Docker runs the process as UID 0 (root).
  • To run as non-root (recommended, limits the damage if a container is compromised):
docker run -d --name sleeper-uid --user=1001 ubuntu sleep 3600   # runtime override
docker exec -it sleeper-uid ps aux                               # USER shows 1001 (no passwd entry → numeric)
FROM ubuntu
USER 1000          # baked into the image
docker build -t my-ubuntu-image .
docker run -d --name my-sleeper my-ubuntu-image sleep 3600
docker exec -it my-sleeper ps aux          # runs as UID 1000
  • If the UID has no entry in the image's /etc/passwd, tools show the number instead of a name. Add the entries if the app needs usernames.

Is container root = host root? No, it is restricted by:

  • Linux capabilities: root's power is split into fine-grained privileges (e.g. CAP_NET_BIND_SERVICE, CAP_SYS_TIME, CAP_SYS_ADMIN). Docker drops many high-risk capabilities by default. Full list: man 7 capabilities.
  • seccomp: syscall filtering; dangerous syscalls are blocked by default.
  • LSMs: AppArmor / SELinux profiles, where available.
FlagEffectUse
--cap-add=<CAP>Adds a capabilityOnly what the app needs, e.g. NET_ADMIN for advanced networking
--cap-drop=<CAP>Removes a capabilityRemove capabilities the workload doesn't need
--privilegedGrants a broad set of capabilities and disables much of the confinementOnly for special cases that truly need host-level access
docker run --cap-add=NET_ADMIN ubuntu sleep 3600
docker run --cap-drop=SYS_TIME ubuntu sleep 3600
docker run --privileged ubuntu sleep 3600          # avoid
  • Kubernetes uses the same kernel primitives. Its securityContext (§7.25) maps to these Docker options and adds cluster-level policy on top.

[!NOTE] Beyond the course: current Kubernetes nodes run containerd or CRI-O (no dockershim since v1.24). The concepts are the same, but on nodes you inspect containers with crictl, not docker.

[!WARNING] Exam traps / remember: prefer specific --cap-add / --cap-drop over --privileged. The default user is root (UID 0) unless --user or USER sets another.

[!CAUTION] Correction: user-namespace isolation is opt-in (Docker userns-remap; Kubernetes spec.hostUsers: false). Without it, container UID 0 maps to host UID 0, with capabilities and other isolation still limiting access.

[!NOTE] Beyond the course: unlike Docker’s default, Kubernetes requires securityContext.seccompProfile.type: RuntimeDefault (or the kubelet’s seccomp-default setting) to use the runtime default seccomp profile. Pod Security Standards restricted requires an allowed profile. See seccomp.

7.25 Security Contexts

Source: KodeKloud notes

Pod vs container level

  • securityContext is the Kubernetes way to set the UID and capabilities that you set with Docker flags in §7.24.
  • You can set it at the Pod level (applies to all containers) or at the container level. For the same field, the container value overrides the Pod value.
FieldLevelEffectExample
runAsUserPod or containerUID for the process (numeric)runAsUser: 1000
runAsGroupPod or containerPrimary GID for the process (numeric)runAsGroup: 3000
fsGroupPod onlyGID that owns mounted volumesfsGroup: 2000
runAsNonRootPod or containerRefuse to run as UID 0runAsNonRoot: true
capabilities (add/drop)Container onlyAdd or drop Linux capabilitiesadd: ["NET_ADMIN"]
# 1) Pod-level: applies to every container
apiVersion: v1
kind: Pod
metadata:
  name: secctx-pod
spec:
  securityContext:
    runAsUser: 1000
    runAsGroup: 3000
    fsGroup: 2000
  containers:
  - name: ubuntu
    image: ubuntu:20.04
    command: ["sleep", "3600"]
# 2) Container-level override
apiVersion: v1
kind: Pod
metadata:
  name: secctx-container-override
spec:
  securityContext:
    runAsUser: 1000
    runAsGroup: 3000
  containers:
  - name: ubuntu
    image: ubuntu:20.04
    command: ["sleep", "3600"]
    securityContext:
      runAsUser: 2000          # wins over Pod-level 1000
      runAsNonRoot: true
# 3) Capabilities (container level only)
apiVersion: v1
kind: Pod
metadata:
  name: secctx-capabilities
spec:
  containers:
  - name: net-tools
    image: alpine:3.18
    command: ["sleep", "3600"]
    securityContext:
      capabilities:
        add: ["NET_ADMIN", "SYS_TIME"]   # names without CAP_ prefix
        drop: ["MKNOD"]

Best practices

  • Use numeric UIDs/GIDs for runAsUser, runAsGroup and fsGroup.
  • Set runAsNonRoot: true wherever the app supports it.
  • Least privilege: add only the capabilities you need and explicitly drop the rest. Set them per container, so other containers in the Pod don't get them.
  • Enforce rules cluster-wide with Pod Security Admission, Gatekeeper or another policy controller (Pod Security Standards). See §3.22 for admission controllers.

[!CAUTION] Correction: the original says runAsNonRoot: true "will fail pod creation" when the process would run as UID 0. In fact the API server accepts the Pod. The kubelet then refuses to start the container and the status shows CreateContainerConfigError ("container has runAsNonRoot and image will run as root"). It also fails if the image's USER is a non-numeric name, because the kubelet cannot verify it. Fix it with a numeric runAsUser or an image with a numeric non-root USER.

Viewing and troubleshooting

kubectl get pod secctx-pod -o yaml       # full resolved securityContext
kubectl describe pod secctx-pod          # summary + Events (start/validation failures)
kubectl exec secctx-pod -- id            # Beyond the course: effective uid/gid/groups
  • If a Pod fails security validation, check the Pod events and the admission controller logs (if one is configured).

[!NOTE] Beyond the course: other common fields are allowPrivilegeEscalation, privileged and readOnlyRootFilesystem (container level) and seccompProfile (Pod or container level). PodSecurityPolicy was removed in v1.25; use Pod Security Admission namespace labels (pod-security.kubernetes.io/enforce: restricted).

[!WARNING] Exam traps / remember:

  • capabilities is container-level only. The Pod-level securityContext has no such field, so kubectl apply fails with "unknown field".
  • fsGroup is Pod-level only.
  • The container value wins over the Pod value for the same field. Other Pod-level fields still apply.
  • You cannot edit securityContext on a running Pod. Export the YAML, then delete and recreate the Pod (§7.26). For a Deployment, edit the template.

Add to the field table or the capabilities YAML comment: # course slide example: capabilities: add: ["MAC_ADMIN"] (container-level only; same idea as docker --cap-add MAC_ADMIN)

7.26 Lab Solution Security Contexts

Source: KodeKloud notes

Theory: §7.25.

#TaskTechnique → answer
1User running sleep in ubuntu-sleeperkubectl exec -it ubuntu-sleeper -- whoami → root. If the UID has no passwd entry, whoami fails; use id -u (numeric UID)
2Make it run as UID 1010Export, add Pod-level securityContext.runAsUser: 1010, delete and recreate → id -u = 1010
3multi-pod.yaml: user of container webPod runAsUser: 1001, web has container-level runAsUser: 1002 → 1002 (container overrides)
4User of container sidecarNo container-level context → inherits Pod's → 1001
5Run as root + add SYS_TIMERemove runAsUser (default is root) or set it to 0; add container securityContext.capabilities.add: ["SYS_TIME"]
6Also add NET_ADMINSame list: add: ["SYS_TIME", "NET_ADMIN"]
kubectl get pod ubuntu-sleeper -o yaml > ubuntu-sleeper.yaml   # export, then edit
kubectl delete pod ubuntu-sleeper --force                      # most Pod fields can't be changed in place
kubectl apply -f ubuntu-sleeper.yaml
kubectl exec -it ubuntu-sleeper -- id -u                       # verify UID
# Q2: Pod-level UID
apiVersion: v1
kind: Pod
metadata:
  name: ubuntu-sleeper
spec:
  securityContext:
    runAsUser: 1010
  containers:
  - name: ubuntu
    image: ubuntu
    command: ["sleep", "4800"]
# Q3/Q4: multi-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: multi-pod
spec:
  securityContext:
    runAsUser: 1001          # sidecar inherits this
  containers:
  - image: ubuntu
    name: web
    command: ["sleep", "5000"]
    securityContext:
      runAsUser: 1002        # web overrides → 1002
  - image: ubuntu
    name: sidecar
    command: ["sleep", "5000"]
# Q5 + Q6: root (no runAsUser) + capabilities on the container
apiVersion: v1
kind: Pod
metadata:
  name: ubuntu-sleeper
spec:
  containers:
  - name: ubuntu
    image: ubuntu
    command: ["sleep", "4800"]
    securityContext:
      capabilities:
        add: ["SYS_TIME", "NET_ADMIN"]   # Q5 = SYS_TIME only; Q6 adds NET_ADMIN

[!CAUTION] Correction: the course YAML keeps the exported volumeMounts entry for kube-api-access but leaves out the matching volumes: entry. As written, that manifest fails validation (volume mount name not found). Either keep the exported file intact or strip the auto-generated fields (volumeMounts for the SA token, terminationMessage*, resources: {}, status), as done above.

[!NOTE] Beyond the course: kubectl replace --force -f ubuntu-sleeper.yaml deletes and recreates in one step. To check capabilities, run kubectl exec ubuntu-sleeper -- grep Cap /proc/1/status (the course suggests capsh, a small privileged debug container if tools are absent, or simply testing an operation that needs SYS_TIME, e.g. date -s).

[!WARNING] Exam traps / remember:

  • --force deletion returns immediately, and the old process may still run on the node for a short time. Prefer graceful deletion when time allows.
  • Capabilities go under the container securityContext, never the Pod one.
  • "Run as root" means removing runAsUser or setting it to 0. A Pod-level runAsUser left in place still applies to the container.

7.27 Network Policies

Source: KodeKloud notes

Ingress vs egress (direction = who initiated)

  • Ingress = traffic arriving at the pod; egress = traffic the pod originates.
  • Classify by the side that opened the connection. Reply packets do not count as the opposite direction.
  • Example 3-tier app: user → web (80) → API (5000) → DB (3306).
StepSourceDestinationPortDirection
1Internet userWeb pod80Ingress to web
2Web podAPI pod5000Egress from web, ingress to API
3API podDB pod3306Egress from API, ingress to DB
  • When locking everything down, allow both ends: egress on the origin pod and ingress on the receiving pod.

Per-tier ingress/egress rules: web in 80 / out 5000, API in 5000 / out 3306, DB in 3306

Cluster networking baseline

  • Every node, pod and Service gets an IP. The CNI must provide pod-to-pod connectivity across nodes without extra routes.
  • Default: all-allow. Any pod can reach any pod or Service in the cluster. NetworkPolicy is how you restrict this.

Default all-allow mesh: every pod on every node can reach every other pod

  • Why bother: least privilege (e.g. frontend must not reach the DB directly), smaller attack surface, trust boundaries between namespaces and services.

NetworkPolicy fundamentals

  • NetworkPolicy, API networking.k8s.io/v1, namespaced. It applies only to pods in its own namespace. A namespaceSelector is how you match peers in other namespaces.
  • spec.podSelector picks the pods the policy applies to, matched by label.
  • spec.policyTypes: Ingress, Egress, or both. These are the directions the policy restricts.
  • spec.ingress[].from / spec.egress[].to list the allowed peers: podSelector, namespaceSelector, ipBlock. ports sets the allowed protocol and port.
  • When a policy selects a pod with type Ingress, every ingress not explicitly allowed is denied. Egress stays open unless some policy with type Egress selects the pod.
  • Allow-lists only. There are no deny rules.
  • Additive. When several policies select the same pod, the allowed traffic is the union of all of them.

Policy around the DB pod: allow ingress only from the API pod on 3306

Example: only the API pod may reach the DB on 3306

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: db-policy
spec:
  podSelector:              # pods this policy protects
    matchLabels:
      role: db
  policyTypes:
  - Ingress                 # restrict inbound only; egress of DB untouched
  ingress:
  - from:
    - podSelector:          # allowed source pods (same namespace)
        matchLabels:
          name: api-pod
    ports:
    - protocol: TCP
      port: 3306            # destination port on the DB pod
kubectl apply -f db-policy.yaml
kubectl get netpol                    # short name: netpol
kubectl describe netpol db-policy     # human-readable "Allowing ingress traffic ..." summary

Field quick reference

FieldPurposeExample
podSelectorPods (same namespace) the policy applies tomatchLabels: { role: db }
policyTypesDirections enforced['Ingress'] or ['Ingress','Egress']
ingress / egressAllowed sources/destinations + portssee YAML above
from / toPeer selectors inside a rulepodSelector, namespaceSelector, ipBlock

Enforcement depends on the CNI

  • The CNI plugin enforces NetworkPolicy. The API object only records the intent.
  • If the CNI doesn't enforce policies, the API server still accepts the object, but nothing is blocked and you get no error. Always test connectivity after applying a policy (e.g. curl or a TCP check between pods).
CNINetworkPolicy support
CalicoFull support, advanced features
CiliumFull support, eBPF-based
Weave NetSupported
Kube-routerSupported
RomanaSupported
Flannel (basic)May not enforce

[!WARNING] Exam traps / remember:

  • The default is all-allow. Pods that no policy selects stay fully open.
  • policyTypes: [Ingress] with no ingress: rules means deny all ingress to the selected pods.
  • Ingress and egress are judged by the initiator. A reply never needs its own rule.
  • Policies only add allowances. You cannot write "deny X" or override another policy's allow.
  • A plain podSelector peer only matches pods in the policy's namespace.
  • A policy accepted on a Flannel-only cluster still enforces nothing.

[!NOTE] Beyond the course:

  • There is no kubectl create networkpolicy imperative command. Write the YAML from the docs page or kubectl explain networkpolicy.spec.ingress.from.
  • If you omit policyTypes, Ingress is always implied, and Egress is added only when an egress: section exists.
  • Flannel by itself never enforces NetworkPolicy. Canal (Flannel + Calico policy) does. Weave Net is no longer maintained since Weaveworks shut down in 2024, so prefer Calico or Cilium.
  • Common building blocks:
# Default deny all ingress + egress for every pod in the namespace
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: prod
spec:
  podSelector: {}          # {} = every pod in the namespace
  policyTypes:
  - Ingress
  - Egress
---
# Allow all ingress to every pod in the namespace (an empty rule matches everything)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-all-ingress
  namespace: prod
spec:
  podSelector: {}
  policyTypes:
  - Ingress
  ingress:
  - {}

7.28 Developing Network Policies

Source: KodeKloud notes

Goal

  • DB pod accepts TCP 3306 only from the API pod. Web → DB is denied. Web ↔ API traffic and all other pods are untouched, because only pods matching the DB selector are affected.

Web-API allowed on 5000, API-DB allowed on 3306, Web-DB denied

Build it in 3 steps

  1. Select the target. Use podSelector with the DB pod's label (role: db).
  2. Pick the direction. The API pod initiates the connection, so Ingress on the DB pod is enough. Responses are allowed automatically (policies are stateful), so no egress rule is needed for replies. Add Egress only if the DB itself opens outbound connections, e.g. pushing backups.
  3. Define sources and ports. Each ingress rule has from (peers) and ports (destination port/protocol on the target pod).
Peer selectorUseExample
podSelectorPods by label (policy's own namespace unless combined with namespaceSelector)matchLabels: { role: api }
namespaceSelectorPods in namespaces with matching labelsmatchLabels: { kubernetes.io/metadata.name: prod }
ipBlockExternal IPs or CIDRs, e.g. hosts outside the clustercidr: 192.168.5.10/32

AND vs OR: placement of the dash

  • Separate items in the from / to list are ORed.
  • podSelector and namespaceSelector inside one list item are ANDed: the source pod must match the label and live in a matching namespace.
  • Writing them as two separate items, with an extra -, makes them an independent OR. Access becomes much broader.

AND vs OR peer logic with three example source pods Diagram (new): one list item ANDs pod and namespace selectors; an extra dash turns them into two ORed peers.

Ingress-only: API pods in prod, OR one external IP

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: db-policy
spec:
  podSelector:
    matchLabels:
      role: db
  policyTypes:
  - Ingress
  ingress:
  - from:
    - podSelector:            # item 1: role=api AND in namespace prod
        matchLabels:
          role: api
      namespaceSelector:      # no dash -> same item as podSelector (AND)
        matchLabels:
          kubernetes.io/metadata.name: prod
    - ipBlock:                # item 2 (OR): one external host
        cidr: 192.168.5.10/32
    ports:
    - protocol: TCP
      port: 3306

Ingress + egress: DB also pushes backups to 192.168.5.10:80

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: db-policy
spec:
  podSelector:
    matchLabels:
      role: db
  policyTypes:
  - Ingress
  - Egress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          role: api
      namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: prod
    ports:
    - protocol: TCP
      port: 3306
  egress:
  - to:                       # `to` takes the same peer types as `from`
    - ipBlock:
        cidr: 192.168.5.10/32
    ports:
    - protocol: TCP
      port: 80                # destination port on the backup server

Testing and checklist

  • Test in dev or staging first. Start throwaway pods with kubectl run and try nc, telnet or a mysql client from the web, API and external test pods. Check both the allowed and the denied paths.
  • If a policy seems to have no effect, check two things: (a) does the CNI support NetworkPolicy, and (b) does the policy actually select the intended pods (labels and namespaces must match).
  • Checklist: the DB pods carry role: db; the API pods carry the expected label and run in the intended namespace; the CNI supports policies; the selector indentation and dashes give the AND/OR logic you intended.

[!WARNING] Exam traps / remember:

  • A stray - before namespaceSelector turns AND into OR and silently lets every pod in that namespace in.
  • Inside a rule, ports and from / to are ANDed: the peer must match and the port must match.
  • podSelector + namespaceSelector inside the same from/to list item are ANDed; separate peer list items (each beginning with -) are ORed.
  • Restricting the DB's ingress doesn't need an egress rule on the DB for replies.

[!NOTE] Beyond the course:

  • The kubernetes.io/metadata.name=<ns> label is added to every namespace automatically (since v1.21), so you can select a namespace by name without labelling it. Check with kubectl get ns --show-labels.
  • namespaceSelector: {} on its own means all pods in all namespaces.
  • ipBlock supports except: [<cidr>] to carve holes. It is meant for cluster-external IPs, because pod IPs are ephemeral.
  • ports[].port can also be a named container port. endPort allows a port range.
  • Quick probes: kubectl run tmp --rm -it --restart=Never --image=busybox -- wget -qO- -T 2 http://<svc>:<port> (HTTP), or kubectl run tmp --rm -it --restart=Never --image=nicolaka/netshoot -- nc -zv -w 2 <ip|svc> 3306 (raw TCP). Add -n <ns> -l role=api to probe as a labelled source pod.

7.29 Lab Solution Network Policies optional

Source: KodeKloud notes

Setup: namespace default, pods external, internal, mysql, payroll; alias k=kubectl. Theory is in §7.27–7.28.

TaskCommand / answerGotcha
List pods and servicesk get pods · k get svcServices: db-service 3306/TCP (ClusterIP, mysql pod); external-service 8080:30080; internal-service 8080:30082; payroll-service 8080:30083 (all NodePort)
How many policies?k get networkpolicies (or k get netpol) → payroll-policy, POD-SELECTOR name=payrollThe POD-SELECTOR column shows which pod the policy applies to
What does it do?k describe netpol payroll-policyRead Allowing ingress traffic / Not affecting egress traffic / Policy Types
Which traffic type?Policy Types: IngressIngress to payroll is restricted; its egress stays open
Impact?Only pods labelled name=internal may reach payroll on 8080/TCPAll other ingress to payroll is dropped
UI connectivity testinternal → payroll:8080 works; external → payroll:8080 times outinternal → external:8080 and internal → mysql:3306 still work, because no egress policy exists yet
Create internal-policyYAML below → k create -f internal-policy.yamlEgress from internal only to payroll:8080 and mysql:3306
Verifyk describe netpol internal-policyExpect Not affecting ingress traffic plus two egress blocks separated by -----

payroll-policy as described:

Spec:
  PodSelector:     name=payroll
  Allowing ingress traffic:
    To Port: 8080/TCP
    From:
      PodSelector: name=internal
  Not affecting egress traffic
  Policy Types: Ingress
DirectionAllowedBlocked
Ingress to payrollFrom name=internal pods to 8080/TCPEverything else, e.g. external → payroll
Egress from payrollEverything—

internal-policy.yaml:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: internal-policy
  namespace: default
spec:
  podSelector:
    matchLabels:
      name: internal          # policy applies to the internal pod
  policyTypes:
  - Egress                    # ingress of internal is not affected
  egress:
  - to:                       # rule 1: payroll on 8080
    - podSelector:
        matchLabels:
          name: payroll
    ports:
    - protocol: TCP
      port: 8080
  - to:                       # rule 2: mysql on 3306
    - podSelector:
        matchLabels:
          name: mysql
    ports:
    - protocol: TCP
      port: 3306
# k describe netpol internal-policy
Spec:
  PodSelector:    name=internal
  Not affecting ingress traffic
  Allowing egress traffic:
    To Port: 8080/TCP
    To:
      PodSelector: name=payroll
    -----------------------
    To Port: 3306/TCP
    To:
      PodSelector: name=mysql
  Policy Types: Egress
  • Result: internal can only reach payroll:8080 and mysql:3306, unless another policy allows more. payroll-policy is unchanged, so payroll still only accepts traffic from internal.

[!WARNING] Exam traps / remember:

  • Setting policyTypes: [Ingress] or [Egress] without matching rules blocks that whole direction.
  • Use two separate egress rules (one to + ports pair per destination). If you put both podSelectors and both ports into one rule, internal could also reach payroll:3306 and mysql:8080, which is broader than asked.
  • Get the labels from k get pods --show-labels before writing selectors. The lab uses name=<pod>.

[!NOTE] Beyond the course: once egress is restricted, DNS lookups also fail, because traffic to CoreDNS on port 53 is blocked. That means names like payroll-service stop resolving, even though IPs still work. KodeKloud's own lab answer adds an egress rule for DNS. Use it whenever the task allows it or the app calls Services by name:

  - ports:                  # extra egress rule: DNS to any destination
    - protocol: UDP
      port: 53
    - protocol: TCP
      port: 53

7.30 2025 Updates Custom Resource Definition CRD

Source: KodeKloud notes

Built-in resources: object in etcd + controller

  • When you create a Deployment, the API server stores the object in etcd. The Deployment controller, a control-plane process, watches it and reconciles: it creates a ReplicaSet, and the ReplicaSet controller creates the Pods.
  • Each built-in kind (ReplicaSet, Deployment, Job, CronJob, StatefulSet, Namespace, …) has a matching controller.
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp-deployment
spec:
  replicas: 3
  selector:
    matchLabels:
      type: front-end
  template:
    metadata:
      name: myapp-pod
      labels:
        type: front-end
    spec:
      containers:
      - name: nginx           # required field (missing in the course YAML)
        image: nginx
kubectl create -f deployment.yml
kubectl get deployments
kubectl delete -f deployment.yml

Built-in resources and their controllers, with etcd as the shared store

[!CAUTION] Correction: the course Deployment lists - image: nginx with no name. A container name is required, so the API server rejects the manifest. The YAML above adds it.

Custom resource example: FlightTicket

apiVersion: flights.com/v1    # <group>/<version> defined by the CRD
kind: FlightTicket
metadata:
  name: my-flight-ticket
spec:
  from: Mumbai
  to: London
  number: 2
  • If you create it before the CRD exists, it fails:
error: unable to recognize "flightticket.yml": no matches for kind "FlightTicket" in version "flights.com/v1"
  • Once the CRD is registered, kubectl create/get/delete work like any other kind. Creating the CR only stores it in etcd. To get side effects, e.g. calling bookflight.com/API, you need a custom controller that watches FlightTicket objects (see §7.31).

The CRD

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: flighttickets.flights.com   # must be <plural>.<group>
spec:
  group: flights.com                # API group -> apiVersion flights.com/v1
  scope: Namespaced                 # or Cluster
  names:
    kind: FlightTicket              # used in YAML `kind:`
    singular: flightticket          # used in CLI output and as an alias
    plural: flighttickets           # used in REST path and `kubectl get flighttickets`
    shortNames:
    - ft                            # kubectl get ft
  versions:
  - name: v1
    served: true                    # exposed via the API
    storage: true                   # version persisted in etcd (exactly one)
    schema:
      openAPIV3Schema:              # server-side validation
        type: object
        properties:
          spec:
            type: object
            properties:
              from:
                type: string
              to:
                type: string
              number:
                type: integer
                minimum: 1
                maximum: 10         # e.g. number: 11 is rejected by the API server
kubectl create -f flightticket-crd.yml
# customresourcedefinition.apiextensions.k8s.io/flighttickets.flights.com created
kubectl create -f flightticket.yml
kubectl get flighttickets          # or: kubectl get ft
kubectl delete -f flightticket.yml
CRD fieldPurposeExample
groupAPI group of the custom resourceflights.com
nameskind, singular, plural, shortNameskind: FlightTicket, plural: flighttickets
scopeNamespaced or ClusterNamespaced
versionsVersions with served / storage flags- name: v1, served: true, storage: true
schemaOpenAPI v3 validationopenAPIV3Schema with spec properties

CRD registers a type; CR is validated and stored; controller watches, calls external API, writes status Diagram (new): the CRD adds the API type, the CR is persisted in etcd, and a custom controller supplies the behaviour.

[!WARNING] Exam traps / remember:

  • A CRD provides API wiring and validation only. It has no business logic; a controller supplies that.
  • metadata.name of the CRD must be exactly <plural>.<group>.
  • The CR's apiVersion is <group>/<version name> from the CRD, e.g. flights.com/v1. The CRD's own apiVersion is apiextensions.k8s.io/v1.
  • kind is CamelCase singular. plural and singular are lowercase.

[!NOTE] Beyond the course:

  • In apiextensions.k8s.io/v1, every version must have a schema.openAPIV3Schema; a CRD without one is rejected. Exactly one version can have storage: true.
  • The CRD object itself is cluster-scoped. scope only controls its CRs.
  • Useful checks:
kubectl get crd                                  # list CRDs
kubectl describe crd flighttickets.flights.com
kubectl api-resources | grep -i flight           # NAME, SHORTNAMES, APIVERSION, NAMESPACED, KIND
kubectl explain flightticket.spec                # works thanks to the schema
  • Built-in controllers are Go code too: controllerKind = apps.SchemeGroupVersion.WithKind("Deployment"), DeploymentController.Run(workers, stopCh) and addReplicaSet(obj) (recognition only; these explain the same watch/reconcile pattern).

CRD diagram filenames are abbreviated: crd.yml = flightticket-crd.yml; ft.yml = flightticket.yml.

7.31 2025 Updates Custom Controllers

Source: KodeKloud notes

What a controller does

  • A controller is a process running a reconcile loop. It watches the API for changes to specific objects and drives actual state toward desired state. Some people call a custom controller an "operator"; see §7.32 for the distinction.
  • For FlightTicket CRs (same YAML as §7.30), a controller would:
    • call the booking API when a ticket is created, and book, modify or cancel as the object changes;
    • write the outcome (confirmed / failed / pending) to .status;
    • retry on transient errors;
    • clean up when a ticket is cancelled or deleted.
  • CRs show no STATUS column in kubectl get by default. Inspect status with kubectl get flightticket my-flight-ticket -o yaml, or define custom printer columns in the CRD.

Why Go and client-go (k8s.io/client-go)

  • You can write a controller in any language. Go is the norm because client-go supplies tested building blocks, leaving you to write only the business logic.
ComponentPurposeNotes
InformerWatches API objects and keeps a local cacheSharedInformer in k8s.io/client-go/tools/cache
WorkqueueSerialises processing; rate-limiting and retriesworkqueue.RateLimitingInterface
ListerFast read-only queries against the cachegenerated, or from the informer factory
ReconcilerYour logic to reach desired statecall booking API, update .status
Status updatesReport observed state back on the CR.status subresource: conditions, message

Workflow from kubernetes/sample-controller

git clone https://github.com/kubernetes/sample-controller.git
cd sample-controller
# edit controller.go -> add FlightTicket logic (call booking API, update status)
go build -o sample-controller .
./sample-controller -kubeconfig=$HOME/.kube/config   # run locally against the cluster
# log: "Setting up event handlers" ... "Starting FlightTicket controller"

Typical Go skeleton (recognition only):

var controllerKind = flights.SchemeGroupVersion.WithKind("FlightTicket")

// Run begins watching and syncing.
func (c *FlightTicketController) Run(workers int, stopCh <-chan struct{})

// Reconcile action for a FlightTicket event.
func (c *FlightTicketController) callBookFlightAPI(obj interface{})
  • Packaging: build a container image and run it in the cluster as a Deployment, or a DaemonSet if needed. In-cluster config makes authentication simpler, and Kubernetes provides scheduling, restarts and HA.
  • kubebuilder or Operator Framework tooling can scaffold the CRD, controller wiring and RBAC boilerplate.

[!WARNING] Exam scope: writing controllers is development work and is not expected in the CKA. You are expected to create and use CRDs, inspect CRs and their .status, and know the flow: CRD → controller watches CRs → acts → updates status.

[!NOTE] Beyond the course: a controller running in-cluster needs a ServiceAccount plus RBAC (Role/ClusterRole) on its CRs, e.g. flighttickets in apiGroup flights.com including flighttickets/status. See §7.16 and §7.18. To make status columns show up, add this to the CRD version:

    subresources:
      status: {}                    # enables the /status subresource
    additionalPrinterColumns:
    - name: Status
      type: string
      jsonPath: .status.state       # shown as a column in `kubectl get ft`

7.32 2025 Updates Operator Framework

Source: KodeKloud notes

Operator = CRD(s) + controller(s), packaged as one deployable unit

  • An operator encodes human operational know-how (install, upgrade, back up, recover) as code that runs in the cluster.
  • Typical responsibilities:
    • provisioning and configuring app instances;
    • automated backups and restores;
    • upgrades and schema migrations;
    • failure detection and remediation;
    • exposing metrics and status.
  • Popular operators on OperatorHub: etcd, MySQL, Prometheus, Grafana, Argo CD, Istio.
ComponentRoleExample
CRDDefines the new API type and schemaFlightTicket CRD
Custom Resource (CR)Instance of the CRD: a request or configa FlightTicket object
ControllerReconciles CRs with the operational logicGo controller for FlightTicket
OperatorPackage of CRDs, controllers and optional subcomponents such as backup/restoreetcd operator → EtcdCluster CRD + controllers

Minimal operator package

  • Contents: CRD YAML (as in §7.30), controller (as in §7.31), and one manifest that deploys both:
kubectl create -f flight-operator.yaml   # CRD(s) + controller Deployment (+ RBAC) in one manifest

[!CAUTION] Correction: the course's "minimal" operator CRD has no schema under versions[]. With apiextensions.k8s.io/v1 the API server rejects that CRD (schemas are required). Use the full CRD from §7.30, which includes openAPIV3Schema.

etcd operator example

  • Installing it registers EtcdCluster, EtcdBackup and EtcdRestore CRDs. The ETCD controller, backup operator and restore operator reconcile them.

etcd operator: EtcdCluster/EtcdBackup/EtcdRestore CRDs mapped to ETCD controller, backup operator, restore operator

Deploying from OperatorHub with OLM

StepWhat it doesCommand
1. Install OLM (Operator Lifecycle Manager)Manages operator install, upgrade and lifecyclecurl -sL .../install.sh | bash -s v0.32.0
2. Install the operatorThrough OLM, or by applying the operator manifest directlykubectl create -f https://operatorhub.io/install/etcd.yaml
3. Create CRsRequest app instanceskubectl apply -f my-etcd-cluster.yaml
# Install OLM (example release)
curl -sL https://github.com/operator-framework/operator-lifecycle-manager/releases/download/v0.32.0/install.sh | bash -s v0.32.0
# Install the etcd operator from OperatorHub
kubectl create -f https://operatorhub.io/install/etcd.yaml
# Check the ClusterServiceVersion (CSV) of installed operators (PHASE should become Succeeded)
kubectl get csv -n my-etcd
  • Operators vary widely. Some only handle install and upgrade; others automate backup, restore and recovery. Before adopting one, read its CRDs, its ClusterServiceVersion (CSV) and its docs.
  • Exam focus: CRDs extend the API, controllers watch and reconcile, and you deploy CRDs and controllers either manually or as an operator.

[!WARNING] Exam traps / remember:

  • Operator ≠ CRD. A CRD alone only stores data. An operator ships the CRD and the controller that acts on it.
  • csv is an OLM resource (ClusterServiceVersion). It exists only after OLM is installed.

[!NOTE] Beyond the course: to install an operator in the exam you usually apply its manifests or a Helm chart, then check the result:

kubectl get crd                  # new CRDs registered?
kubectl get pods -A              # controller pod Running?
kubectl api-resources --api-group=<group>

← 06 Cluster Maintenance · Index · 08 Storage →


Reference

Based on the KodeKloud Certified Kubernetes Administrator (CKA) course notes.

On this page

7.1 Security Section IntroductionSection map7.2 Kubernetes Security PrimitivesHost security (control plane and worker nodes)kube-apiserver: first line of defenceTLS between componentsNetwork security between workloads7.3 AuthenticationWho accesses the clusterAuthentication mechanismsStatic token file7.4 TLS IntroductionGoals of the TLS lessons7.5 TLS BasicsWhy TLSSymmetric vs asymmetric encryptionSSH (asymmetric key auth)HTTPS handshake and CA trustmTLS, signing vs encryption, PKIFile naming convention7.6 TLS in KubernetesThree kinds of certificateComponent → certificate mapCAsNext steps / rules7.7 TLS in Kubernetes Certificate Creation1. CA (self-signed root)2. Client certificates (admin user, components)3. etcd server (and peer) certificates4. kube-apiserver serving cert (SANs)5. kube-apiserver flags that reference certs6. kubelet certificates (per node)7. Inspect7.8 View Certificate DetailsStep 1: how was the cluster built?Step 2: build a certificate inventory (spreadsheet)Step 3: decode each certAudit checklistStep 4: logs for TLS failures7.9 Lab Solution View Certification DetailsDrill: find the files (answers come from the manifests)Drill: read cert details (openssl x509 -in <crt> -text -noout)Drill: troubleshooting scenario 1 (kubectl: "connection refused")Drill: troubleshooting scenario 2 ("TLS handshake timeout")Debug checklist (apiserver ↔ etcd)7.10 Certificates APIWhy a Certificates APIWorkflowCommands: key, CSR, objectsignerName and usagesWhere signing happens7.11 Lab Solution Certificates API7.12 KubeConfigWithout a kubeconfigStructure: clusters, users, contextsView and switchNamespace in a contextCertificate paths vs inline data7.13 Lab Solution KubeConfig7.14 API GroupsThe API surfaceCore vs named groupsDiscoverykubectl proxy7.15 AuthorizationPurposeModesNode authorizerABAC policy fileMultiple modes: evaluation orderConfiguring (systemd-managed API server)7.16 Role Based Access ControlsRole = namespaced set of rulesRoleBinding = attach Role to subjectsView RBAC objectsTest permissions: kubectl auth can-iRestrict to named objects7.17 Lab Role Based Access Controls7.18 Cluster RolesNamespaced vs cluster-scoped resourcesClusterRole + ClusterRoleBindingClusterRoles on namespaced resources7.19 Lab Solution Cluster RolesDrill: give user michelle node accessDrill: give michelle storage access (PVs + StorageClasses)7.20 Service AccountsAccount typesDefault SA and token mountingCreate a custom SA and attach itDisable auto-mounting: automountServiceAccountToken: falseTokens for external use7.21 Lab Solution Service Accounts7.22 Image SecurityImage name formatPrivate registry: Docker vs KubernetesBest practices7.23 Lab Solution Image Security7.24 Prerequisite Security in DockerIsolation with namespacesUsers inside containersIs container root = host root? No, it is restricted by:7.25 Security ContextsPod vs container levelBest practicesViewing and troubleshooting7.26 Lab Solution Security Contexts7.27 Network PoliciesIngress vs egress (direction = who initiated)Cluster networking baselineNetworkPolicy fundamentalsExample: only the API pod may reach the DB on 3306Field quick referenceEnforcement depends on the CNI7.28 Developing Network PoliciesGoalBuild it in 3 stepsAND vs OR: placement of the dashIngress-only: API pods in prod, OR one external IPIngress + egress: DB also pushes backups to 192.168.5.10:80Testing and checklist7.29 Lab Solution Network Policies optional7.30 2025 Updates Custom Resource Definition CRDBuilt-in resources: object in etcd + controllerCustom resource example: FlightTicketThe CRD7.31 2025 Updates Custom ControllersWhat a controller doesWhy Go and client-go (k8s.io/client-go)Workflow from kubernetes/sample-controller7.32 2025 Updates Operator FrameworkOperator = CRD(s) + controller(s), packaged as one deployable unitMinimal operator packageetcd operator exampleDeploying from OperatorHub with OLM