The gate reached stable and left the tree in seven releases, the default it was built to change is still Unconfined at the pin, and the post's two-step recipe now works only if you skip step one — because step one enables a gate that no longer exists, and the kubelet refuses to start¶
Post — Enable seccomp for all workloads with a new v1.22 alpha
feature, 2021-08-25, by Sascha Grunert (Red
Hat) — 267 lines, 11,409 bytes. Two kubelet settings that move every workload on a node from
Unconfined to the container runtime's default seccomp profile, plus an upgrade strategy for doing
it without breaking the applications. One of the two settings has been deleted, the other works
exactly as written, and the thing the post says should be fixed in the long term is still not
fixed.
As written
The post opens on the state of seccomp in Kubernetes after its graduation to GA in v1.19. The
securityContext gained dedicated seccomp fields (:19-23), and a Pod or container can name one of
three profile types (:29-33): Unconfined — seccomp will not be enabled; RuntimeDefault —
the container runtimes default profile will be used; and Localhost — a node local profile will
be applied, which is being referenced by a relative path to the seccomp profile root
(<kubelet-root-dir>/seccomp) of the kubelet. Then the sentence the rest of the post exists to
answer, at :35-39: With the graduation of seccomp, nothing has changed from an overall security
perspective, because Unconfined is still the default. This is totally fine if you consider this from
the upgrade path and backwards compatibility perspective of Kubernetes releases. But it also means
that it is more likely that a workload runs without seccomp at all, which should be fixed in the
long term.
## SeccompDefault to the rescue (:41-60) introduces the v1.22 kubelet feature gate, added in
alpha state as every other new feature, disabled by default and enabled manually for every single
Kubernetes node. What it does is one sentence at :50-51: it just changes the default seccomp
profile from Unconfined to RuntimeDefault. :53-57 warns that the runtime default differs between
CRI-O and containerd and between hardware architectures, while generally speaking, those default
profiles allow a common amount of syscalls while blocking the more dangerous ones.
### Enabling the feature (:62-74) is the operational core, and it is a numbered list of exactly
two items. One: Enable the feature gate by setting the SeccompDefault=true via the command line
(--feature-gates) or the kubelet configuration file. Two: Turn on the feature by enabling the
feature by adding the --seccomp-default command line flag or via the kubelet configuration file
(seccompDefault: true). Then the sentence that makes the pair a pair, at :74: The kubelet will
error on startup if only one of the above steps have been done.
### Trying it out (:76-176) is four manifests and four readings. A one-container Pod with no
securityContext at all (:81-90), then the verification recipe (:99-102): CONTAINER_ID=$(sudo
crictl ps -q --name=test-container) followed by sudo crictl inspect $CONTAINER_ID | jq
.info.runtimeSpec.linux.seccomp, whose output (:104-116) shows defaultAction: SCMP_ACT_ERRNO,
three x86 architectures, and a syscalls array with an SCMP_ACT_ALLOW entry. :118-121 reads
that back as CRI-O and runc having applied the default profile, which denies all syscalls per
default, while allowing commonly used ones. :128-131 adds the limitation: the feature will not
influence any Kubernetes API for now. Therefore, it is not possible to retrieve the used seccomp
profile via kubectl get or describe if the SeccompProfile field is unset within the
SecurityContext. Then a two-container Pod (:138-152) where test-container-nginx sets type:
Unconfined and test-container-redis sets nothing, and three jq assertions (:156-176) proving
the first is unprofiled, the second is not, and the sandbox itself runs with the default.
### Upgrade strategy (:178-252) is the half of the post that is advice rather than mechanism, in
four stages. Turning on the gate changes nothing observable — Enabling the feature gate at the
kubelet level will not turn on the feature, but will make it possible (:185-188). Testing the
application offers five bullets, two _Recommended_ and three _Optional_: analyse the code or run
it under strace (:197-203); set the profile manually and roll it out (:205-207); run an
end-to-end suite with RuntimeDefault (:209-211); build a custom profile whose defaultAction is
SCMP_ACT_LOG rather than SCMP_ACT_ERRNO so blocked syscalls are logged instead of denied, then
read them out of /var/log/audit/audit.log or /var/log/syslog by type=SECCOMP or type=1326
(:213-224); or use the Security Profiles Operator, which makes the above mentioned manual log
investigation obsolete (:226-229). Deploying follows from the test result (:237-242), and the
last stage enables the kubelet setting on a per-node basis to reduce the overall risk of missing a
syscall (:244-252).
As it runs now — the mechanism is stable, one of the two switches has been deleted, and the default has not moved.
The gate is gone, and the post's first instruction is the one that breaks the kubelet. The
SeccompDefault gate file is still on disk at the pin, but it declares removed: true and _build:
{list: never, render: false}, so it is one of the 230 gate files that exist without being published
as pages. Its stages end at v1.28. A kubelet at v1.37 does not know the name, and a featureGates
entry naming it is a startup error rather than a no-op — which inverts :74 exactly. The post says
the kubelet errors if you do only one of the two steps; at the pin the kubelet errors if you do
both, and starts cleanly if you do only the second.
The second switch works verbatim. seccompDefault is a live bool in the kubelet configuration
(reference/config-api/kubelet-config.v1beta1.md:1706-1711), described in the post's own words —
SeccompDefault enables the use of RuntimeDefault as the default seccomp profile for all workloads
— and --seccomp-default is a live flag
(reference/command-line-tools-reference/kubelet.md:823-826), notable for not carrying the
(DEPRECATED: This parameter should be set via the config file…) marker that 100 of that page's 126
flag rows carry. The post's second instruction can be pasted into a v1.37 node unchanged.
The default is still Unconfined. kubelet-config.v1beta1.md:1711 reads Default: false, and
tasks/administer-cluster/kubelet-config-file.md:285 shows "seccompDefault": false in a kubelet's
own dumped configuration. The project's security checklist puts it in the imperative at
concepts/security/security-checklist.md:165-167: Since Kubernetes 1.27, you can enable the use of
RuntimeDefault as the default seccomp profile for all workloads — can enable, with the
corresponding checklist item at :106 still unticked by default. The gate went alpha, beta, stable
and out of the tree; the behaviour it gated is still opt-in per node. That is the whole finding:
what graduated was the ability to change the default, not the default.
The post's API limitation is now documentation, and it names the same tool.
tutorials/security/seccomp.md:440-447: Enabling the feature will neither change the Kubernetes
securityContext.seccompProfile API field nor add the deprecated annotations of the workload. This
provides users the possibility to rollback anytime without actually changing the workload
configuration. Tools like crictl inspect can be used to verify which seccomp profile is being used
by a container. The post's warning at :128-131 and its verification recipe at :99-102 were both
absorbed, the second one by name. Four years on there is still no way to ask the API what profile a
container actually got.
That invisibility has a consequence the post could not have seen. Pod Security admission reads
the spec, not the node, and the Restricted standard at
concepts/security/pod-security-standards.md:435-443 requires that the Seccomp profile must be
explicitly set to one of the allowed values. Both the Unconfined profile and the absence of a
profile are prohibited. So a node with seccompDefault: true running a Pod with no seccomp field
gives that Pod the RuntimeDefault profile and fails Restricted — the node default cannot satisfy
an admission rule that is written against the manifest. The refusal itself is walked by the runc
CVE exercise; what belongs here is the asymmetry: this feature
makes a cluster more secure without making a single Pod compliant.
Three of the post's five testing bullets have become documentation; two have not. The
Unconfined override and the custom profile survive as tutorials/security/seccomp.md:453-456, the
per-node rollout as :458-460, and the SCMP_ACT_LOG idea is now an entire tutorial step rather
than an optional bullet — :201-306 builds a Pod around an audit.json profile for exactly that
purpose. What did not survive is the tooling. strace appears zero times under content/en/docs at
the pin. The Security Profiles Operator is named in three documentation files —
concepts/security/linux-kernel-security-constraints.md:229,
concepts/security/security-checklist.md:168-169 and tutorials/security/apparmor.md:239 — none of
which is the seccomp tutorial or the seccomp reference. The operator the post recommends for
profiling is still recommended, from pages about other things.
And the advice underneath those bullets was reversed.
concepts/security/linux-kernel-security-constraints.md:100-114 is a section titled Considerations
for seccomp which opens seccomp is a low-level security configuration that you should only
configure yourself if you require fine-grained control over Linux syscalls, lists three risks of
doing it at scale — Configurations might break during application updates, Attackers can still
use allowed syscalls to exploit vulnerabilities, Profile management for individual applications
becomes challenging at scale — and then states a Recommendation: Use the default seccomp
profile that's bundled with your container runtime. If you need a more isolated environment,
consider using a sandbox, such as gVisor. The post's answer to a workload that RuntimeDefault
breaks is a hand-built profile, twice. The pin's answer is: do not hand-build profiles, and if the
runtime default is not enough, change the sandbox instead of the filter.
The documentation the post points at grew a page that did not exist, and kept talking about the
gate. Seccomp now has a reference page of its own, reference/node/seccomp.md, which lists
four places a profile can be specified (:19-25) against the post's two — adding init
containers and ephemeral containers — and states two things the post never mentions: that
container-level fields beat the Pod-level value while unset containers inherit it (:36-38), and
that It is not possible to apply a seccomp profile to a Pod or container running with privileged:
true set in the container's securityContext. Privileged containers always run as Unconfined.
(:40-44). Meanwhile the tutorial the docs send readers to still speaks of the removed gate in four
places: the SeccompDefault feature at :454 and :476, and this feature gate at :459 and
:475, the last inside a kind example pinned to kindest/node:v1.28.0 — the last release in
which the gate existed.
The lab's runtime is not the post's. The output at :104-116 is CRI-O and runc, as :118-119
says. This lab runs containerd, and both the post at :53-54 and reference/node/seccomp.md:55-59
warn that the defaults differ between runtimes and between their releases. The profile you read in
step 5 will not match the post's byte for byte, and that is the post being right rather than wrong.
The runtime itself, the socket and crictl are the CRI
exercise's subject; the kubelet
drop-in directory used here to make a one-node change is the PID-limiting
exercise's. Setting the seccomp field explicitly, as part of a
Restricted-compliant Pod, is the eleven-ways exercise's.
Release dates and the pressure behind each are in
research/blog-era-translation.md.
The diff, and why — four cases, and the interesting one is the stasis.
Broke: step one of a two-step recipe. The gate was removed after v1.28 and the kubelet no longer
recognises the name, so a configuration that follows the post exactly does not start. This is the
ordinary fate of an alpha-era instruction and the post could not have written it differently. What
makes it worth a measurement is the shape of the failure: the post's own safety sentence at :74
promised an error for the half-configured case, and the pin delivers an error for the
fully-configured one. A reader following the post and reading its warning would conclude they had
done too little, when they had done one thing too many.
Overtaken by stasis: the default. The post's premise is that Unconfined is the default and
should be fixed in the long term. Sixteen versions later the gate that was created to fix it has
completed its whole life cycle — alpha at v1.22, beta and on at v1.25, stable at v1.27, deleted
after v1.28 — and seccompDefault still defaults to false. Nothing regressed and nothing was
abandoned: the graduation was real, the feature works, and the project simply never took the step of
changing what happens to a node where nobody has configured anything. Set beside the other stasis
case in this year — the scheduler that still currently does not account for swap, walked by the
swap exercise — the pattern is that Kubernetes graduates
capabilities readily and changes defaults almost never, because a default is the one thing an
upgrade cannot opt out of.
Retired by being agreed with. The verification recipe, the API-invisibility warning, the
per-node rollout, the Unconfined escape hatch and the SCMP_ACT_LOG trick are all in the
documentation now, in some places nearly word for word, and crictl inspect is still the only way
to answer the question the post asks. An alpha announcement whose mechanics were absorbed this
completely is unusual in this archive; compare it with the same year's swap post, where the caveats
were absorbed and the configuration was replaced.
Never absorbed: the tooling, and the strategy underneath it. strace is nowhere in the
documentation, the Security Profiles Operator is named only from pages about other subjects, and the
pin's own recommendation section argues against the custom-profile path that two of the post's five
bullets recommend. This is not the documentation disagreeing with a fact; it is the documentation
having changed its mind about what an operator should do when RuntimeDefault breaks an
application. The post says profile it and fix the profile. The pin says use the runtime's profile,
and if that is not enough, use a stronger sandbox.
The ladder
One gate, transcribed from its stages: list parsed as YAML, plus the file-level flag that ends it.
Seven releases end to end, and the second row is the one to read carefully. From v1.25 the gate
defaulted to true, and nothing observable changed on any node — because, as the post itself says
at :185-188, Enabling the feature gate at the kubelet level will not turn on the feature, but
will make it possible. A gate defaulting to true while the behaviour stayed off is the clearest
example in this year of why a gate's defaultValue is not the feature's default: the gate governed
availability, and seccompDefault governed behaviour. When the gate went stable at v1.27 and
was deleted after v1.28, availability became unconditional and the behaviour did not move at all.
The gate's body text says what it always said: Enables the use of RuntimeDefault as the default
seccomp profile for all workloads. locked never appears in this file — it never needed to,
because the whole ladder is about whether the option exists, and the option is now simply a field.
Topology — pair: a control-plane node at
10.10.10.130 and a worker at 10.10.10.131, brought up with the five provision
steps. Two nodes are the subject, not a convenience. The
post's whole upgrade strategy is per-node — :244-252 says to enable the kubelet setting on a
per-node basis, and tutorials/security/seccomp.md:458-460 still recommends a subset of your
nodes — and a claim about a subset of nodes cannot be shown on one node. Here the setting goes on
the worker only, so the same manifest with no securityContext at all gets RuntimeDefault on one
node and Unconfined on the other, and the difference is invisible to the API on both. Everything
is driven from the control-plane host; the worker is reached over ssh zain@10.10.10.131 for the
kubelet's own files, and crictl is needed on both nodes.
Do
- Establish that neither node has this feature on, from the kubelet's own view rather than from a file. Do it for both nodes, because the rest of the exercise is a comparison:
for N in $(kubectl get nodes -o jsonpath='{.items[*].metadata.name}'); do
echo "== $N"
kubectl get --raw "/api/v1/nodes/$N/proxy/configz" \
| python3 -c 'import sys,json; c=json.load(sys.stdin)["kubeletconfig"]; print(" seccompDefault:", c.get("seccompDefault")); print(" featureGates:", c.get("featureGates", {}))'
done
sudo grep -o -- '--seccomp-default[^ ]*' /var/lib/kubelet/kubeadm-flags.env || echo "flag not set on the control plane"
ssh zain@10.10.10.131 "sudo grep -o -- '--seccomp-default[^ ]*' /var/lib/kubelet/kubeadm-flags.env || echo 'flag not set on the worker'"
- Apply the post's first manifest exactly as
:81-90gives it, then read the effective profile with the post's own recipe from:99-102, on whichever node the scheduler chose:
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
name: test-pod
spec:
containers:
- name: test-container
image: nginx:1.21
EOF
kubectl wait --for=condition=Ready pod/test-pod --timeout=120s
kubectl get pod test-pod -o jsonpath='{.spec.nodeName}{"\n"}'
sudo crictl --runtime-endpoint unix:///run/containerd/containerd.sock inspect \
"$(sudo crictl ps -q --name=test-container)" | jq .info.runtimeSpec.linux.seccomp
- Now do the post's step 1 on the worker, and only step 1. A drop-in file keeps the change to one node and one line, the way the PID-limiting exercise does it:
ssh zain@10.10.10.131 'sudo mkdir -p /etc/kubernetes/kubelet.conf.d && \
printf "apiVersion: kubelet.config.k8s.io/v1beta1\nkind: KubeletConfiguration\nfeatureGates:\n SeccompDefault: true\n" \
| sudo tee /etc/kubernetes/kubelet.conf.d/20-seccomp-gate.conf'
ssh zain@10.10.10.131 'sudo systemctl restart kubelet; sleep 20; systemctl is-active kubelet'
ssh zain@10.10.10.131 'sudo journalctl -u kubelet --since "-2min" --no-pager | grep -i -m5 "feature\|seccomp\|unrecognized"'
- Take the gate away and do the post's step 2 on its own. One file replaces the other, and this is the configuration the post says is incomplete:
ssh zain@10.10.10.131 'sudo rm -f /etc/kubernetes/kubelet.conf.d/20-seccomp-gate.conf && \
printf "apiVersion: kubelet.config.k8s.io/v1beta1\nkind: KubeletConfiguration\nseccompDefault: true\n" \
| sudo tee /etc/kubernetes/kubelet.conf.d/21-seccomp-default.conf'
ssh zain@10.10.10.131 'sudo systemctl restart kubelet; sleep 20; systemctl is-active kubelet'
kubectl get --raw "/api/v1/nodes/$(kubectl get nodes -o jsonpath='{.items[?(@.spec.taints)]}' >/dev/null; echo node2)/proxy/configz" 2>/dev/null | head -c 0
WORKER=$(kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{" "}{end}' | tr ' ' '\n' | grep -v "$(kubectl get nodes -l node-role.kubernetes.io/control-plane -o jsonpath='{.items[0].metadata.name}')" | head -1)
kubectl get --raw "/api/v1/nodes/$WORKER/proxy/configz" \
| python3 -c 'import sys,json; print("seccompDefault:", json.load(sys.stdin)["kubeletconfig"].get("seccompDefault"))'
- Run the post's manifest again, pinned to the worker this time, and read what the runtime actually applied. The post printed CRI-O's profile; this is containerd's:
kubectl delete pod test-pod --wait
kubectl apply -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
name: test-pod
spec:
nodeName: $WORKER
containers:
- name: test-container
image: nginx:1.21
EOF
kubectl wait --for=condition=Ready pod/test-pod --timeout=120s
ssh zain@10.10.10.131 'sudo crictl inspect "$(sudo crictl ps -q --name=test-container)" \
| jq "{defaultAction: .info.runtimeSpec.linux.seccomp.defaultAction, architectures: .info.runtimeSpec.linux.seccomp.architectures, rules: (.info.runtimeSpec.linux.seccomp.syscalls | length), allowed: (.info.runtimeSpec.linux.seccomp.syscalls[0].names | length)}"'
- The same manifest on the control-plane node, and the same question asked of the API on both. This is the per-node claim and the invisibility claim in one step:
CP=$(kubectl get nodes -l node-role.kubernetes.io/control-plane -o jsonpath='{.items[0].metadata.name}')
kubectl apply -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
name: test-pod-cp
spec:
nodeName: $CP
tolerations: [{operator: "Exists"}]
containers:
- name: test-container-cp
image: nginx:1.21
EOF
kubectl wait --for=condition=Ready pod/test-pod-cp --timeout=120s
sudo crictl inspect "$(sudo crictl ps -q --name=test-container-cp)" | jq '.info.runtimeSpec.linux.seccomp == null'
kubectl get pod test-pod test-pod-cp -o jsonpath='{range .items[*]}{.metadata.name}{": "}{.spec.securityContext}{" / "}{.spec.containers[0].securityContext}{"\n"}{end}'
kubectl describe pod test-pod | grep -i seccomp || echo "describe says nothing about seccomp"
- The post's two-container manifest from
:138-152, pinned to the worker, with its threejqassertions from:156-176. One container opts out, one inherits the node default, and the sandbox has a profile of its own:
kubectl apply -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
name: test-pod-multi
spec:
nodeName: $WORKER
containers:
- name: test-container-nginx
image: nginx:1.21
securityContext:
seccompProfile:
type: Unconfined
- name: test-container-redis
image: redis:6.2
EOF
kubectl wait --for=condition=Ready pod/test-pod-multi --timeout=180s
ssh zain@10.10.10.131 'sudo crictl inspect $(sudo crictl ps -q --name=test-container-nginx) | jq ".info.runtimeSpec.linux.seccomp == null"
sudo crictl inspect $(sudo crictl ps -q --name=test-container-redis) | jq ".info.runtimeSpec.linux.seccomp != null"
sudo crictl inspectp $(sudo crictl pods -q --name test-pod-multi) | jq ".info.runtimeSpec.linux.seccomp != null"'
- The exception the post does not mention, which
reference/node/seccomp.md:40-44states flatly. One field defeats the node default silently, with nothing in the Pod's seccomp configuration to show for it:
kubectl apply -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
name: test-pod-priv
spec:
nodeName: $WORKER
containers:
- name: test-container-priv
image: nginx:1.21
securityContext:
privileged: true
EOF
kubectl wait --for=condition=Ready pod/test-pod-priv --timeout=120s
ssh zain@10.10.10.131 'sudo crictl inspect $(sudo crictl ps -q --name=test-container-priv) \
| jq "{seccomp: .info.runtimeSpec.linux.seccomp, privileged: .info.config.linux.security_context.privileged}"'
- Inheritance, on the field surface the pin lists as four and the post lists as two. A Pod-level
Unconfinedwith an init container that overrides it — the shape of the pin's ownfields.yamlexample, minus theephemeralContainersblock that example admits cannot be set at create time:
kubectl apply -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
name: test-pod-inherit
spec:
nodeName: $WORKER
securityContext:
seccompProfile:
type: Unconfined
initContainers:
- name: init-container
image: nginx:1.21
command: ["sleep", "300"]
restartPolicy: Always
securityContext:
seccompProfile:
type: RuntimeDefault
containers:
- name: main-container
image: nginx:1.21
EOF
kubectl wait --for=condition=Ready pod/test-pod-inherit --timeout=120s
ssh zain@10.10.10.131 'for c in init-container main-container; do printf "%s: " "$c"; sudo crictl inspect $(sudo crictl ps -q --name=$c) | jq -c ".info.runtimeSpec.linux.seccomp.defaultAction // \"none\""; done'
kubectl explain pod.spec.securityContext.seccompProfile | head -8
-
Offline, read the paper trail this feature left. Two spellings of one switch, a gate file that exists without being a page, and a recommendation that points away from the post's advice:
cd /path/to/kubernetes/website grep -rln --include='*.md' -- '--seccomp-default' content/en/docs grep -rln --include='*.md' 'seccompDefault' content/en/docs grep -c 'removed: true' content/en/docs/reference/command-line-tools-reference/feature-gates/SeccompDefault.md grep -rl 'removed: true' content/en/docs/reference/command-line-tools-reference/feature-gates/*.md | wc -l grep -n 'SeccompDefault\|feature gate\|kindest/node' content/en/docs/tutorials/security/seccomp.md grep -rn --include='*.md' 'strace' content/en/docs | wc -l sed -n '100,114p' content/en/docs/concepts/security/linux-kernel-security-constraints.md
Expect
Step 1 prints seccompDefault: False and an empty featureGates map for both nodes, and no
--seccomp-default in either kubeadm-flags.env. A cluster installed by kubeadm at v1.37 gives
every workload Unconfined, which is the state the post set out to change and the state the project
has left in place. Note that configz answers this question directly: the effective kubelet
configuration is readable through the API even though the resulting profile is not.
Step 2 prints a node name and then null. The post's manifest, unmodified, on a current cluster,
produces a container with no seccomp filter at all — the same result it would have produced in 2021
before enabling anything. Keep the node name: if the scheduler put this Pod on the worker, steps 5
and 6 will still be a clean comparison because the setting is not on yet.
Step 3 is the measurement. systemctl is-active kubelet should print something other than active,
and the journal should name SeccompDefault as an unrecognised or unsupported feature gate — the
exact wording varies by release, so copy it down. If your build instead logs a warning and starts,
record that instead; either way what you have is a configuration that follows the post's numbered
list precisely and leaves the node worse than not following it at all. The worker will go NotReady
a minute or two later, which is the visible cost of the post's step 1 at the pin.
Step 4 starts cleanly and prints seccompDefault: True. The post's :74 — The kubelet will error
on startup if only one of the above steps have been done — is now false in the most direct way
available: doing only one of the two steps is the only configuration that works. The WORKER shell
variable is worth keeping for the rest of the exercise; the first kubectl get --raw line in that
step exists only to fail harmlessly if your node names differ from the derivation that follows.
Step 5 prints a defaultAction of SCMP_ACT_ERRNO, a list of architectures, a rules count and an
allowed count. The architecture list and both counts will differ from the post's output, which
came from CRI-O and runc; containerd's default profile is its own file with its own history. What
matches is the structure and the meaning: deny by default, allow a named set. This is the whole
feature, and it took one line in one file on one node.
Step 6 prints true for the control-plane container's profile being null, and then the comparison
that makes the point: the same image, the same empty securityContext, two nodes, two different
seccomp postures. The jsonpath line prints nothing for either Pod's securityContext, and
describe says nothing about seccomp for either. Everything the API can tell you about these two
Pods is identical, and the thing that differs is not in the API. That is :128-131 holding four
years later, and it is also why Pod Security admission cannot credit this node for the profile it is
applying.
Step 7 prints true, true, true — the post's three assertions, unchanged, on a containerd
cluster with the gate it names deleted. The container that asked for Unconfined got Unconfined,
the container that asked for nothing got the node default, and the sandbox got the node default too.
The precedence rule behind the middle result is reference/node/seccomp.md:36-38: container fields
beat the Pod-level value, and unset containers inherit.
Step 8 prints a seccomp of null next to privileged: true. A privileged container on a node
with seccompDefault: true is unfiltered, and nothing in the Pod's seccomp configuration says so —
there is no field to inspect, no event, and no warning. The post's it just changes the default
seccomp profile from Unconfined to RuntimeDefault has one exception it does not name, and this is
the cheapest possible demonstration that a node-level default is not a guarantee.
Step 9 prints SCMP_ACT_ERRNO for the init container and none for the main one, then kubectl
explain describing seccompProfile. The Pod-level Unconfined propagated to the main container,
the init container's own field won, and the node default never entered into it — an explicit
Pod-level setting beats seccompDefault everywhere it appears. The init container has to carry
restartPolicy: Always to still be running when you inspect it; that is the sidecar form, and it is
one of the two field locations the pin lists that the post's era did not have.
Step 10 is a count of the feature's footprint. The hyphenated flag appears in two files, the
camel-case field in three, and only tutorials/security/seccomp.md contains both; the gate file
declares removed: true and is one of 230 that do; the last release in which that gate existed was
v1.28; the tutorial names the removed gate at :454 and :476 and calls it a feature gate twice
more at :459 and :475, with its kind example pinned to kindest/node:v1.28.0 at :484 and
:492 (the v1.18.2 image at :145 is an unrelated docker ps printout); strace returns a
count of 0; and the sed prints a recommendation to use the runtime's bundled profile or a
sandbox rather than a profile of your own. Read that last block against the post's _Optional_
bullets and you are reading a change of strategy, not a change of fact.
Read on
-
tutorials/security/seccomp.md:201-306is the post's fourth testing bullet promoted to a tutorial step: anaudit.jsonprofile whosedefaultActionisSCMP_ACT_LOG, applied to a Pod, with the resulting syscall log read off the node. It is the only place in the tree that does what the post suggests, and it needsLocalhostprofiles on disk — which is a different mechanism from everything this exercise touched. -
reference/node/seccomp.md:69-146is the profile format itself: the OCI JSON scheme,defaultErrnoRet, and the eight action entries fromSCMP_ACT_ERRNOtoSCMP_ACT_LOG, with the note at:128-135that several of them may not work depending on the runtime, the OCI runtime or the kernel version. Read it next to the pin's ownfields.yamlexample, whose comment at:9-10admits that theephemeralContainersblock it contains cannot be used when creating a Pod. -
concepts/security/linux-kernel-security-constraints.mdis the page that did not exist when this post was written and now frames the whole subject: seccomp beside capabilities, AppArmor and SELinux, with a table at:175-213setting out what each mechanism is for. Its seccomp considerations at:100-114are the strategic reversal named above; read:221-230after them, where the Security Profiles Operator is recommended for managing custom configurations of all three mechanisms at scale — the post's own recommendation, arrived at from the opposite direction. -
Three pieces of this exercise's machinery belong to other rows. The CRI exercise installs
crictland explains the socket and the RPCs behind every reading in this file. The PID-limiting exercise is the kubelet drop-in directory and the per-node configuration change as a subject in itself. The eleven-ways exercise setsseccompProfile: RuntimeDefaultexplicitly in every manifest, which is the other half of this story: the field, rather than the node default. -
Unanswerable from the pin: why the default never moved. The gate completed its ladder, the documentation has recommended the behaviour since v1.27, and nothing in the tree argues against flipping
seccompDefaulttotrue— but nothing in the tree explains why it was not flipped either, and a gate file records stages, not reasons. Two smaller questions go the same way: whether theprivilegedexception atreference/node/seccomp.md:40-44held in v1.22, which the pin states without dating; and why the Security Profiles Operator, the post's own recommendation, is absent from both seccomp pages while being linked from three other security pages.
Teardown
This exercise left five Pods and one kubelet drop-in file. The drop-in is the only durable change, and it is on the worker:
kubectl delete pod test-pod test-pod-cp test-pod-multi test-pod-priv test-pod-inherit --ignore-not-found
ssh zain@10.10.10.131 'sudo rm -f /etc/kubernetes/kubelet.conf.d/20-seccomp-gate.conf /etc/kubernetes/kubelet.conf.d/21-seccomp-default.conf'
ssh zain@10.10.10.131 'sudo systemctl restart kubelet; sleep 20; systemctl is-active kubelet'
for N in $(kubectl get nodes -o jsonpath='{.items[*].metadata.name}'); do
kubectl get --raw "/api/v1/nodes/$N/proxy/configz" | python3 -c 'import sys,json; print("seccompDefault:", json.load(sys.stdin)["kubeletconfig"].get("seccompDefault"))'
done
kubectl get nodes
Both nodes should report seccompDefault: False again and both should be Ready. Nothing was
installed and no API object outlived the Pods, so this returns the cluster to the state step 1
measured — a cluster where every workload runs Unconfined unless its author says otherwise. Leave
the two guests up if the next exercise in this year wants them; otherwise destroy
them.