Notes07-Security
Security
Who may call the API, how TLS and kubeconfig identify them, how RBAC limits verbs, and how securityContext and NetworkPolicy constrain a running Pod.
Security Quick Notes
The request path
Every API call passes four gates, in order:
- Authentication — who is this? Failure is
401. - Authorization — may this identity perform this verb? Failure is
403. - Admission — is the object allowed to exist (ResourceQuota, Pod Security, mutating and validating webhooks)?
- etcd — the apiserver writes the object.
Kubernetes users are not objects in etcd the way Pods are. A user is a string the authenticator produced (alice, or a certificate CN). ServiceAccounts are objects.
Authentication
Method | Where you see it |
|---|---|
X.509 client certificate | kubeconfig |
Bearer token |
|
ServiceAccount | Every Pod can mount a token. The username is |
OIDC / webhook | External identity. The apiserver is configured with issuer flags or a webhook file. |
- Anonymous: If anonymous auth is enabled, unauthenticated calls arrive as
system:anonymousin groupsystem:unauthenticated. kubeadm locks this down.system:public-info-viewercan still read some non-sensitive discovery data. system:masters: A group that the apiserver authorizes as cluster-admin before RBAC. kubeadm’ssuper-admin.confcertificate is in this group. Day-to-day admin work uses a kubeconfig bound through RBAC (kubeadm:cluster-admins/cluster-admin).
TLS material on a kubeadm control plane
/etc/kubernetes/pki/ca.crt cluster CA
/etc/kubernetes/pki/apiserver.crt API server certificate
/etc/kubernetes/pki/apiserver-kubelet-client.* apiserver → kubelet
/etc/kubernetes/pki/etcd/ca.crt etcd CA (separate)
/etc/kubernetes/pki/front-proxy-ca.crt aggregation proxy
- The API server certificate must list every name clients use in Subject Alternative Names: the advertise IP,
kubernetes,kubernetes.default, the ClusterIP of thekubernetesService, and the load-balancer hostname if you set--control-plane-endpoint. A missing SAN shows up asx509: certificate is valid for …. - Client CA is
--client-ca-fileon the apiserver. A client cert signed by a different CA is not a user, even if the file looks right in kubeconfig.
Inspect a certificate:
openssl x509 -in /etc/kubernetes/pki/apiserver.crt -noout -text
openssl x509 -in /etc/kubernetes/pki/apiserver.crt -noout -dates
kubeadm certs check-expiration
Look at Subject, Issuer, Not After, and X509v3 Subject Alternative Name.
Certificates API
A user generates a key and a CSR. A signer (a controller, or you with kubectl certificate approve) issues a certificate signed by the cluster CA.
openssl genrsa -out alice.key 2048
openssl req -new -key alice.key -out alice.csr -subj "/CN=alice/O=developers"
cat <<EOF | kubectl apply -f -
apiVersion: certificates.k8s.io/v1
kind: CertificateSigningRequest
metadata:
name: alice
spec:
request: $(base64 < alice.csr | tr -d '\n')
signerName: kubernetes.io/kube-apiserver-client
usages: ["client auth"]
EOF
kubectl certificate approve alice
kubectl get csr alice -o jsonpath='{.status.certificate}' | base64 -d > alice.crt
kubectl certificate approve alicekubectl get csr alice -o jsonpath='{.status.certificate}' | base64 -d > alice.crt
signerName: kubernetes.io/kube-apiserver-clientis the signer for client certificates a human will put in kubeconfig.usagesmust includeclient auth. A cert without that usage will not authenticate.- Groups come from
O=in the subject.CN=is the username. Encode them in the CSR. You cannot add groups later in the CertificateSigningRequest object. - Approve is not RBAC. Approval means “this certificate may exist.” The RoleBinding is what lets
alicelist Pods.
kubeconfig
A kubeconfig file has three lists: clusters (API URL + CA), users (cert or token), contexts (which user talks to which cluster, plus a default namespace).
kubectl config view
kubectl config current-context
kubectl config get-contexts
kubectl config use-context prod-admin
kubectl config set-cluster lab \
--server=https://10.0.0.10:6443 \
--certificate-authority=/etc/kubernetes/pki/ca.crt \
--embed-certs=true
kubectl config set-credentials alice \
--client-certificate=alice.crt \
--client-key=alice.key \
--embed-certs=true
kubectl config set-context alice@lab \
--cluster=lab --user=alice --namespace=shop
kubectl config use-context alice@lab
--embed-certs=truewrites the PEM into the file so you can copy one kubeconfig to another machine.KUBECONFIG: A colon-separated (Linux) list of files. kubectl merges them.echo $KUBECONFIGbefore you edit~/.kube/config. If the variable points somewhere else, edits to the default file never take effect.- Several clusters in one shell:
kubectl config use-context <name>selects the cluster, thenkubectl get nodesconfirms you are looking at it. An explicit-noverrides the namespace stored on the context.
API groups
Authorization names a group, a resource, and a verb.
Group | Example resources |
|---|---|
| pods, services, configmaps, secrets, namespaces, nodes, persistentvolumes |
| deployments, replicasets, daemonsets, statefulsets |
| roles, rolebindings, clusterroles, clusterrolebindings |
| networkpolicies, ingresses |
| jobs, cronjobs |
| storageclasses, csidrivers |
| certificatesigningrequests |
kubectl api-resources
kubectl api-resources --namespaced=false
kubectl api-versions
kubectl explain networkpolicy.spec --api-version=networking.k8s.io/v1
api-resourcesprints the name you put in a Role (pods, notpod;deployments, notdeployment). The short name (deploy,netpol,csr) works with kubectl and is the wrong string inside a Role rule.- Subresources are
pods/log,pods/exec,deployments/scale.getonpodsdoes not includepods/log.
Authorization modes
The apiserver flag --authorization-mode is a comma list. kubeadm enables Node and RBAC.
- Node: kubelets may only touch objects for their own node (Pods bound here, that node’s leases).
- RBAC: The mode you configure with Role and ClusterRole.
- AlwaysAllow would skip the check. You will not enable that on purpose.
RBAC
Four objects:
Object | Scope | Binds |
|---|---|---|
Role | One namespace | Rules only |
RoleBinding | One namespace | A subject to a Role or ClusterRole, in this namespace |
ClusterRole | Cluster | Rules that can be reused, or that name cluster-scoped resources |
ClusterRoleBinding | Cluster | A subject to a ClusterRole, in every namespace |
A ClusterRole referenced by a RoleBinding grants those rules only in the binding’s namespace. That is how the built-in edit and view ClusterRoles are applied per namespace.
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: pod-reader
namespace: shop
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: read-pods
namespace: shop
subjects:
- kind: User
name: alice
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: pod-reader
apiGroup: rbac.authorization.k8s.io
roleRefis immutable. A typo means delete the binding and create it again.subjects[].nameis case-sensitive and must match the certificate CN or the ServiceAccount name.- ServiceAccount subject:
kind: ServiceAccount,name: deployer,namespace: shop. OmitapiGroup(it is""for ServiceAccounts). - Group subject:
kind: Group,name: developers,apiGroup: rbac.authorization.k8s.io. This matches theO=from the client certificate. resourceNames: Optional allow-list of object names.resourceNames: ["blue"]lets the verbs apply only to that object.liststill returns an empty list for other names rather than a useful catalog;geton any other name is forbidden.- Verbs:
get,list,watch,create,update,patch,delete,deletecollection. Wildcard*exists and is howcluster-adminis written. - Resources
*andapiGroups*` together grant everything in RBAC. Prefer a built-in ClusterRole when that is really what you want.
Built-in ClusterRoles you can bind and should not reinvent:
ClusterRole | Rough scope |
|---|---|
cluster-admin | All verbs on all resources, all namespaces |
admin | All namespaced objects inside one namespace, including Roles |
edit | Workloads, services, config in one namespace. No Role changes |
view | Read namespaced objects. Secrets are excluded |
kubectl get role,rolebinding -n shop
kubectl describe role pod-reader -n shop
kubectl auth can-i create deployments -n shop
kubectl auth can-i delete nodes --as alice
kubectl auth can-i list pods -n shop --as system:serviceaccount:shop:deployer
auth can-i asks the API and prints yes or no. Run it as the user or ServiceAccount you just bound before you treat the Role as done.
Cluster-scoped example:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: node-reader
rules:
- apiGroups: [""]
resources: ["nodes"]
verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: alice-nodes
subjects:
- kind: User
name: alice
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: node-reader
apiGroup: rbac.authorization.k8s.io
Nodes are cluster-scoped. A Role in shop cannot grant nodes.
Security context
Settings that change the process Linux sees. Pod-level fields are defaults. Container-level fields override them.
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 2000
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: myapp:1.2
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
add: ["NET_BIND_SERVICE"]
Field | Effect |
|---|---|
runAsUser / runAsGroup | UID and GID of the process |
runAsNonRoot | Kubelet refuses to start the container if the image would run as UID 0 |
fsGroup | Volume files are group-owned by this GID so the process can read a PVC |
privileged | Essentially host root inside the container. Rare and broad |
allowPrivilegeEscalation | Whether the process may gain privileges (no setuid climb when |
readOnlyRootFilesystem | Root FS is read-only. Pair with an |
capabilities | Fine-grained root powers. Drop |
hostNetwork / hostPID / hostIPC | Share the node’s namespaces. hostNetwork Pods use the node IP |
Pod Security (the replacement for PodSecurityPolicy) is a built-in admission controller. Label a namespace:
kubectl label namespace shop \
pod-security.kubernetes.io/enforce=baseline \
pod-security.kubernetes.io/enforce-version=latest
Profiles: privileged (no restrictions), baseline (blocks host namespaces, privileged containers, and common breakouts), restricted (adds non-root, dropped capabilities, seccomp). enforce rejects. warn and audit report.
ServiceAccounts and image pull
- Default: Every namespace has a
defaultServiceAccount. Pods that omitspec.serviceAccountNameuse it. - Token: Projected into the Pod at
/var/run/secrets/kubernetes.io/serviceaccount/.automountServiceAccountToken: falseon the Pod or the ServiceAccount leaves it out. Set that on workloads that never call the API. - Dedicated accounts: One ServiceAccount per app, bound to a narrow Role. The default account should stay near-empty of bindings.
spec:
serviceAccountName: deployer
automountServiceAccountToken: true
imagePullSecrets:
- name: regcred
imagePullSecretsreferences a Secret of typekubernetes.io/dockerconfigjson. Create it withkubectl create secret docker-registry. You can also attachimagePullSecretsto the ServiceAccount so every Pod using that account pulls from the private registry.imagePullPolicy:Always(what:latestdoes),IfNotPresent,Never(the image must already be on the node).Neveris how you run a locally loaded image with no registry pull.
NetworkPolicy
NetworkPolicy is namespace-scoped and selects Pods with podSelector. It needs a CNI that implements it (Calico, Cilium, the reference plugins that advertise NetworkPolicy). Flannel alone does not enforce it. If the plugin ignores the object, the YAML applies and traffic still flows.
Default with zero policies: all ingress and all egress are allowed.
Once a Pod is selected by any policy:
- If that policy lists
policyTypes: ["Ingress"](or has aningresskey), ingress is allowed only by those rules. An emptyingress: []denies all ingress. - Egress stays open until a selected policy lists
Egress.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: web-allow-app
namespace: shop
spec:
podSelector:
matchLabels:
app: web
policyTypes: ["Ingress", "Egress"]
ingress:
- from:
- podSelector:
matchLabels:
app: api
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: edge
ports:
- protocol: TCP
port: 80
egress:
- to:
- podSelector:
matchLabels:
app: db
ports:
- protocol: TCP
port: 5432
- ports: # DNS
- protocol: UDP
port: 53
- protocol: TCP
port: 53
podSelectorwith nonamespaceSelector: Pods in this namespace.namespaceSelectoralone: All Pods in namespaces that match.- Both in the same
fromitem: Pods that match the labels and live in a matching namespace. - Separate list items are ORed.
ipBlock: CIDR, withexceptto carve out addresses. MixingipBlockwithpodSelectorin one rule is legal and easy to get wider than you meant. Prefer pod and namespace selectors for east-west traffic.- A deny-all ingress is
podSelector: {}(every Pod in the namespace),policyTypes: ["Ingress"], andingress: []. - DNS: An egress deny that forgets UDP/TCP 53 makes every name lookup fail. Allow CoreDNS (kube-system) or port 53 before you allow application ports.
- Services: Policy is enforced on Pod IPs. You still target the Service in the app. The policy selects the backend Pods, not the Service object.
Check from a throwaway Pod (nicolaka/netshoot or busybox) with wget/nc against the target. kubectl describe networkpolicy shows the selectors. It does not show a packet counter.