← CCNP Security: SCOR core and operations
13 / 25 · 55 MIN

Workloads: restrictions and visibility

Exercise container runtime controls and distinguish restriction, compatibility and telemetry coverage.

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))
IN PRACTICE

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

Take this idea with you

Restrictions need demonstrable effects and a compatible application contract.

Create account

Reference: Running containers · 350-701 SCOR v2.0, effective 2026-08-27; core component of CCNP Security

CCNP® and Cisco® are registered trademarks of Cisco Systems, Inc. and/or its affiliates. bigsavant.com is an independent preparation platform and is not affiliated with, associated with, sponsored, authorised or endorsed by Cisco. Content and questions are original, are not official exam questions, and completing our tests does not award or guarantee any certification. Names are used only to identify the subject. All other trademarks belong to their respective owners.