A Kubernetes securityContext configures process identity and selected operating-system constraints. It can reduce what a compromised application is permitted to do, but it does not establish a complete security boundary by itself. Start with the operations the application actually needs, configure the appropriate fields, and test both a permitted operation and a forbidden one in the resulting container.
Put each setting at the right scope
Pod-level settings can supply user and group identity, shared-volume group ownership behavior and a seccomp profile. Container-level settings can override applicable defaults and configure capabilities, privilege escalation and a read-only root filesystem. Check the field’s actual API scope; placing a familiar name under the wrong securityContext does not create an equivalent constraint.
runAsUser selects a numeric user, while runAsNonRoot requires the container to satisfy a non-root policy. These settings complement image metadata rather than trusting a username’s appearance. Volume permissions must also permit the selected identity to perform required work. For an emptyDir used below, fsGroup supplies group access so the application can write its designated working directory.
Linux capabilities divide privileges that would otherwise be associated broadly with root. Dropping ALL removes those capabilities from the container’s configured set. Standard containers do not inherit every Linux capability by default, and a container running as root is not automatically proof of a successful host escape. Review the actual effective privileges rather than using either shortcut.
When it breaks
allowPrivilegeEscalation false sets the no_new_privs constraint for the process. It restricts gaining privileges through executing programs, such as set-user-ID or file-capability transitions. It does not revoke every privilege the process already has or replace application authorization. Inspect the process flag and effective capabilities; do not attribute every denied operation to this one field.
readOnlyRootFilesystem makes the container’s root filesystem read-only. Other mounts can remain writable, including explicitly supplied working volumes. This does not imply a blanket no-execution policy: an interpreter can read and run code stored in a writable volume. It also does not stop network requests or remove access to already mounted data. Read-only storage is one constrained operation, not proof that a compromise cannot progress.
Seccomp filters system calls, but RuntimeDefault depends on the runtime’s supplied profile. A process reporting seccomp filter mode confirms an active filtering mechanism, not that every undesirable syscall or attack path is blocked. Ordinary Linux runtimes can enforce these fields; a special sandbox is not required merely for the settings to have an effect, though stronger isolation has separate design considerations.
Conflicting identity settings can pass API admission but prevent container startup. An image may also expect to write caches or temporary files under its root filesystem. Diagnose startup state and denied paths, then provide only the working storage the application requires. Avoid solving every permission failure by restoring root, privilege escalation or a writable root filesystem.
Check it quickly
Use a disposable Linux Kubernetes cluster, kubectl and python:3.11-slim-bookworm available on its nodes. Save the script as security-context.py and run python3 security-context.py KUBECONFIG_PATH CONTEXT with explicit test-cluster arguments. The account needs namespace creation and deletion permissions. The recorded server was Kubernetes 1.34.11 using kind’s standard runtime configuration.
import json
import subprocess
import sys
import time
from uuid import uuid4
assert len(sys.argv) == 3, 'usage: python3 example.py DISPOSABLE_KUBECONFIG CONTEXT'
base = ['kubectl', '--kubeconfig', sys.argv[1], '--context', sys.argv[2], '--request-timeout=30s']
image = 'python:3.11-slim-bookworm'
def k(*args, payload=None, expected=0):
result = subprocess.run(base + list(args), input=json.dumps(payload) if payload is not None else None,
capture_output=True, text=True, timeout=150)
if expected is not None:
assert result.returncode == expected, result.stderr
return result
def obj(namespace, kind, name=None, selector=None):
args = ['get', kind, '-n', namespace, '-o', 'json']
if name:
args.append(name)
if selector:
args += ['-l', selector]
return json.loads(k(*args).stdout)
def apply(namespace, data):
return k('apply', '-n', namespace, '-f', '-', payload=data)
def wait_for(read, ready, seconds=120):
until = time.monotonic() + seconds
while time.monotonic() < until:
value = read()
if ready(value):
return value
time.sleep(0.5)
raise AssertionError('expected Kubernetes condition did not arrive')
def pod_ready(pod):
return any(c['type'] == 'Ready' and c['status'] == 'True'
for c in pod.get('status', {}).get('conditions', []))
def spec(script='import time; time.sleep(600)'):
return {'automountServiceAccountToken': False, 'terminationGracePeriodSeconds': 1,
'containers': [{'name': 'app', 'image': image, 'imagePullPolicy': 'IfNotPresent',
'command': ['python3', '-c', script],
'resources': {'requests': {'cpu': '10m', 'memory': '24Mi'},
'limits': {'cpu': '200m', 'memory': '96Mi'}}}]}
namespace = 'notes-security-' + uuid4().hex[:8]
k('create', 'namespace', namespace)
try:
probe = '''from pathlib import Path
import errno,json,os,subprocess,time
status=dict(line.split(':',1) for line in Path('/proc/self/status').read_text().splitlines() if ':' in line)
try:
Path('/tmp/forbidden-write').write_text('x')
raise AssertionError('root filesystem unexpectedly writable')
except OSError as error:
assert error.errno==errno.EROFS
try:
os.setuid(0)
raise AssertionError('unexpected UID escalation')
except OSError as error:
assert error.errno==errno.EPERM
work=Path('/work'); (work/'data').write_text('writable')
(work/'example.py').write_text("print('interpreted from the writable volume')")
ran=subprocess.check_output(['python3',str(work/'example.py')],text=True).strip()
result={'uid':os.getuid(),'gid':os.getgid(),'cap_eff':int(status['CapEff'].strip(),16),
'no_new_privs':int(status['NoNewPrivs']),'seccomp':int(status['Seccomp']),
'root_write_errno':errno.EROFS,'uid_zero_denied':True,
'volume_value':(work/'data').read_text(),'writable_volume_code':ran}
assert result['uid']==result['gid']==10001
assert result['cap_eff']==0 and result['no_new_privs']==1 and result['seccomp']==2
print(json.dumps(result),flush=True); time.sleep(600)
'''
podspec = spec(probe)
podspec['securityContext'] = {'runAsNonRoot': True, 'runAsUser': 10001,
'runAsGroup': 10001, 'fsGroup': 10001, 'seccompProfile': {'type': 'RuntimeDefault'}}
podspec['volumes'] = [{'name': 'work', 'emptyDir': {}}]
c = podspec['containers'][0]
c['securityContext'] = {'allowPrivilegeEscalation': False, 'readOnlyRootFilesystem': True,
'capabilities': {'drop': ['ALL']}}
c['volumeMounts'] = [{'name': 'work', 'mountPath': '/work'}]
apply(namespace, {'apiVersion': 'v1', 'kind': 'Pod', 'metadata': {'name': 'restricted'}, 'spec': podspec})
wait_for(lambda: obj(namespace, 'pod', 'restricted'), pod_ready)
observed = json.loads(wait_for(lambda: k('logs', '-n', namespace, 'restricted').stdout.strip(), bool))
conflicting = spec()
conflicting['securityContext'] = {'runAsNonRoot': True, 'runAsUser': 0}
apply(namespace, {'apiVersion': 'v1', 'kind': 'Pod', 'metadata': {'name': 'conflict'}, 'spec': conflicting})
refused = wait_for(lambda: obj(namespace, 'pod', 'conflict'), lambda p:
any(c.get('state', {}).get('waiting', {}).get('reason') == 'CreateContainerConfigError'
for c in p.get('status', {}).get('containerStatuses', [])))
waiting = refused['status']['containerStatuses'][0]
assert 'runAsUser breaks non-root policy' in waiting['state']['waiting']['message']
assert waiting['restartCount'] == 0
print(json.dumps({'runtime_observation': observed,
'conflicting_uid_accepted_by_api_but_container_start_refused': True,
'container_escape_resistance_tested': False}))
finally:
k('delete', 'namespace', namespace, '--wait=true', '--timeout=90s')
The restricted Pod reports UID and GID 10001, effective capabilities zero, NoNewPrivs one and seccomp mode two. Writing under the root filesystem’s /tmp fails with EROFS, and changing to UID zero fails with EPERM. Those results reflect the combined identity and privilege settings, not an isolated test of each field’s individual effect.
The process successfully writes a value under /work and invokes Python on a file it wrote there. This deliberately demonstrates the allowed working-volume behavior and the limit of the read-only-root claim. A second Pod combines runAsNonRoot with runAsUser zero: the API accepts it, but kubelet reports CreateContainerConfigError before the container starts.
All assertions passed, and the namespace was removed. The fixture verifies these concrete runtime constraints and startup failure. It does not test container escape resistance, every seccomp rule, hostile multi-tenancy or the application’s authorization model.
Sources
- Kubernetes: Security Context, Pod and container security fields and runtime effects, checked 2026-09-13.
- Linux: No New Privileges Flag, restrictions on privilege gains through exec, checked 2026-09-13.
- Linux: capabilities(7), separate capability privileges and process capability sets, checked 2026-09-13.