1. Translate a need into separate controls
A fictional batch service only needs to read image code and write temporary results. Define process identity, Linux capabilities, writable paths, networking and resource limits separately. Non-root reduces privileges but does not make every path immutable: an ordinary user can write to directories allowing it. A read-only root filesystem also does not eliminate explicitly writable volumes or tmpfs mounts. Start from the application contract and the image actually executed. Record image identifier, command, user and parameters. A convenient tag alone does not establish that two experiments used identical content.
2. Repeat the lab without creating exposure
The Python program uses an already-present image pinned by full ID, with pull=never. It creates two temporary containers, both using UID/GID 65532, dropped capabilities, no-new-privileges, network=none, 64 MiB and a 32-process limit. The second adds a read-only root and a 1 MiB /scratch tmpfs. It mounts no Mac files, publishes no ports and downloads no images. Inside containers it observes /proc, interfaces, flags and controlled writes. This environment’s kernel exposes inactive tunnel interfaces; the correct check confirms no active external interfaces or IPv4 routes rather than inferring connectivity from an interface name.
3. Interpret observed effects
In the baseline, writing /tmp/dr-rootfs-probe succeeds despite non-root. In the restricted variant, the same write fails with EROFS while /scratch accepts the synthetic marker. Direct execution of a benign script in /scratch fails under noexec. However, /bin/sh can read that same file and produce the expected marker. Therefore, noexec is not an approved-program list and does not prevent all interpretation of data as code. Effective and bounding capability sets are empty and raw-socket creation fails. These observations verify concrete properties; they do not demonstrate universal prevention of container escape or every execution technique.
4. Design temporary state and recovery
The restricted container exits and starts again. Its previous tmpfs marker is absent, and root writing remains denied. That suits disposable data but would fail a requirement to preserve a financial result. If the application needs persistence, design storage, permissions, encryption and recovery separately; do not describe tmpfs as backup. Documentation also warns that tmpfs data may be written to swap, so volatility does not prove secure erasure. NoNewPrivs=1 prevents privilege gains associated with execve under the documented mechanism; it does not mean every process is unprivileged or that every form of credential change is prevented.
5. Compatibility and evidence for RUN
Confirm writable paths and permissions with the application owner before making root immutable. EROFS may show that a control works while the execution contract is incomplete. Keep CPU, memory and process limits in handover, distinguishing inspected configuration from an unperformed load test. For Secure Workload, check agent version, supported platform and capture per interface; an eBPF issue can remove visibility without eliminating communications. The local experiment installed no such agent. Summary: link each control to the property it actually measures, preserve application requirements and do not treat missing telemetry as proof of silence.
"""Run short, unprivileged offline containers from an already cached image only."""
import hashlib
import json
from pathlib import Path
import subprocess
import uuid
IMAGE='sha256:2d9aefe2fef018a7eb2c13064c89c71929800fd2e5dccdbf52ea5da5bb8d929a'
PROBE=r'''
import errno,json,os,pathlib,platform,socket,subprocess
p=pathlib.Path('/scratch/marker');previous=p.exists;status={}
for line in pathlib.Path('/proc/self/status').read_text.splitlines:
if ':' in line:
k,v=line.split(':',1);status[k]=v.strip
try:
pathlib.Path('/tmp/dr-rootfs-probe').write_text('synthetic')
root_write=dict(ok=True)
except OSError as e:root_write=dict(ok=False,errno=e.errno)
raw=None
try:
raw=socket.socket(socket.AF_INET,socket.SOCK_RAW,socket.IPPROTO_ICMP)
raw_result=dict(ok=True)
except OSError as e:raw_result=dict(ok=False,errno=e.errno)
finally:
if raw:raw.close
tmp=None
if pathlib.Path('/scratch').exists:
p.write_text('synthetic marker');script=pathlib.Path('/scratch/example.sh');script.write_text('#!/bin/sh\nprintf local-marker\n');script.chmod(0o700)
try:
result=subprocess.run([str(script)],capture_output=True,text=True,timeout=2);direct=dict(returncode=result.returncode,stdout=result.stdout)
except OSError as e:direct=dict(errno=e.errno)
via=subprocess.run(['/bin/sh',str(script)],capture_output=True,text=True,timeout=2)
tmp=dict(previousMarker=previous,writeRead=p.read_text,direct=direct,viaInterpreter=dict(returncode=via.returncode,stdout=via.stdout))
print(json.dumps(dict(uid=os.getuid,gid=os.getgid,python=platform.python_version,kernel=platform.release,interfaces={name:dict(flags=int(pathlib.Path('/sys/class/net',name,'flags').read_text.strip,16),state=pathlib.Path('/sys/class/net',name,'operstate').read_text.strip)for _,name in socket.if_nameindex},ipv4Routes=[line for line in pathlib.Path('/proc/net/route').read_text.splitlines[1:]if line.strip],noNewPrivs=status['NoNewPrivs'],capEff=status['CapEff'],capBnd=status['CapBnd'],rootWrite=root_write,rawSocket=raw_result,tmp=tmp,mounts=[line.split[:4]for line in pathlib.Path('/proc/mounts').read_text.splitlinesif line.split[1]=='/scratch'])))
'''
def command(args, check=True):
r=subprocess.run(['docker',*args],capture_output=True,text=True,timeout=30)
if check and r.returncode:raise RuntimeError('docker '+args[0]+': '+r.stderr)
return r
def run:
image=json.loads(command(['image','inspect',IMAGE]).stdout)[0]
if image['Id']!=IMAGE:raise RuntimeError('Unexpected image')
names=[];checks=[];observations={};tag='dr-cnwr-'+uuid.uuid4.hex[:12]
def check(name,ok):
if not ok:raise AssertionError(name)
checks.append(name)
common=['--pull=never','--network=none','--user=65532:65532','--cap-drop=ALL','--security-opt=no-new-privileges:true','--memory=64m','--memory-swap=64m','--pids-limit=32','--cpus=0.5','--label=dr.lab=ccnp-workload-response']
try:
for mode in ['baseline','restricted']:
name=tag+'-'+mode;names.append(name);extra=[]if mode=='baseline'else['--read-only','--tmpfs=/scratch:rw,noexec,nosuid,nodev,size=1048576,mode=1777']
r=command(['run','--name',name,*common,*extra,IMAGE,'python','-c',PROBE]);o=json.loads(r.stdout);info=json.loads(command(['inspect',name]).stdout)[0];observations[mode]=o
check(mode+': non-root UID/GID',o['uid']==65532 and o['gid']==65532)
check(mode+': no new privileges observed',o['noNewPrivs']=='1')
check(mode+': effective and bounding capabilities empty',int(o['capEff'],16)==0 and int(o['capBnd'],16)==0)
check(mode+': no active non-loopback interfaces',all(name=='lo' or not (v['flags']&1)for name,v in o['interfaces'].items))
check(mode+': no IPv4 routes',o['ipv4Routes']==[])
check(mode+': Docker network mode none',info['HostConfig']['NetworkMode']=='none')
check(mode+': raw socket refused',o['rawSocket']==dict(ok=False,errno=1))
check(mode+': image pinned without pull',info['Image']==IMAGE)
check(mode+': process exited successfully',info['State']['Status']=='exited'and info['State']['ExitCode']==0)
check(mode+': resource limits configured',info['HostConfig']['Memory']==67108864 and info['HostConfig']['MemorySwap']==67108864 and info['HostConfig']['PidsLimit']==32 and info['HostConfig']['NanoCpus']==500000000)
check(mode+': no host bind mounts',not info['HostConfig'].get('Binds') and all(m.get('Type')!='bind'for m in info.get('Mounts',[])))
b=observations['baseline'];h=observations['restricted'];check('non-root baseline still writes root filesystem tmp',b['rootWrite']==dict(ok=True))
check('read-only root refuses tmp write with EROFS',h['rootWrite']==dict(ok=False,errno=30))
check('explicit tmpfs supports required scratch write',h['tmp']['writeRead']=='synthetic marker')
check('fresh scratch has no previous marker',h['tmp']['previousMarker']is False)
check('noexec prevents direct execution',h['tmp']['direct']==dict(errno=13))
check('noexec does not prevent interpreter reading script',h['tmp']['viaInterpreter']==dict(returncode=0,stdout='local-marker'))
mount=h['mounts'][0];check('scratch is tmpfs with intended flags',mount[2]=='tmpfs'and {'rw','nosuid','nodev','noexec'}<=set(mount[3].split(',')))
restarted=json.loads(command(['start','-a',names[-1]]).stdout);observations['restart']=restarted
check('tmpfs marker absent after stop/start',restarted['tmp']['previousMarker']is False)
check('restriction still effective after restart',restarted['rootWrite']==dict(ok=False,errno=30)and restarted['tmp']['direct']==dict(errno=13))
finally:
for name in names:
# Only unique names created by this run; never remove other containers or images.
command(['rm','-f',name],check=False)
check('owned containers removed',all(command(['inspect',name],check=False).returncode!=0 for name in names))
return dict(passed=len(checks),failed=0,checks=checks,observations=observations,imageId=IMAGE,repoDigests=image.get('RepoDigests',[]),architecture=image['Architecture'],dockerVersion=json.loads(command(['version','--format','{{json.Server}}']).stdout)['Version'],scriptSHA256=hashlib.sha256(Path(__file__).read_bytes).hexdigest,actualContainerExecution=True,actualCiscoAgent=False,actualMalwareExecuted=False,hostBindMountsUsed=False,newImagesDownloaded=False,resourceStressTested=False)
if __name__=='__main__':print(json.dumps(run,indent=2))
Same UID 65532: writable /tmp in baseline; EROFS on restricted root; writable /scratch discarded after stop/start.
Common pitfalls
Non-root as immutability; noexec as interpreter prohibition; tmpfs as persistence or secure erasure; configured limits as a load test.
Related topics: DevSecOps · Linux capabilities · Observability
Restrictions need demonstrable effects and a compatible application contract.
Reference: Running containers · 350-701 SCOR v2.0, effective 2026-08-27; core component of CCNP Security