← CCNP Security: núcleo SCOR e operação
13 / 18 · 55 MIN

Workloads: restrições e visibilidade

Executa controlos de runtime em containers e distingue restrição, compatibilidade e cobertura de telemetria.

1. Traduzir uma necessidade em controlos distintos

Um serviço de batch fictício só precisa de ler o código da imagem e escrever resultados temporários. Define separadamente identidade do processo, capacidades Linux, caminhos de escrita, rede e limites de recursos. Non-root reduz privilégios, mas não torna todos os caminhos imutáveis: um utilizador normal pode escrever em diretórios que o permitem. Um filesystem de raiz só de leitura também não elimina volumes ou tmpfs explicitamente graváveis. Começa pelo contrato da aplicação e pela imagem efetivamente executada. Guarda o identificador da imagem, comando, utilizador e parâmetros. Uma tag conveniente não demonstra, sozinha, que dois ensaios usaram o mesmo conteúdo.

2. Repetir o laboratório sem criar exposição

O programa Python usa uma imagem já presente, fixada pelo ID completo, com pull=never. Cria dois containers temporários, ambos com UID/GID 65532, capacidades removidas, no-new-privileges, network=none, 64 MiB e limite de 32 processos. O segundo acrescenta raiz só de leitura e tmpfs /scratch de 1 MiB. Não monta ficheiros do Mac, não publica portas e não descarrega imagens. Dentro dos containers observa /proc, interfaces, flags e escritas controladas. O kernel deste ambiente expõe interfaces de túnel inativas; a verificação correta confirma ausência de interfaces externas ativas e de rotas IPv4, em vez de inferir conectividade pelo nome de uma interface.

3. Interpretar os efeitos observados

No baseline, escrever /tmp/dr-rootfs-probe funciona apesar de non-root. Na variante restrita, a mesma escrita falha com EROFS, enquanto /scratch aceita o marcador sintético. A execução direta de um script benigno em /scratch falha por noexec. Porém, /bin/sh consegue ler esse mesmo ficheiro e produzir o marcador esperado. Logo, noexec não é uma lista de programas autorizados nem impede toda a interpretação de dados como código. Os conjuntos de capacidades efetivas e de limite estão vazios e a criação de um raw socket falha. Estas observações verificam propriedades concretas; não demonstram prevenção universal de fuga de container ou de todas as técnicas de execução.

4. Projetar estado temporário e recuperação

O container restrito termina e volta a arrancar. O marcador anterior de tmpfs não aparece, e a escrita na raiz continua recusada. Esse comportamento serve dados descartáveis, mas falharia um requisito de conservar um resultado financeiro. Se a aplicação precisa de persistência, desenha armazenamento, permissões, cifragem e recuperação próprios; não declares que tmpfs oferece backup. A documentação também alerta para possível escrita de tmpfs em swap, pelo que volatilidade não prova apagamento seguro. NoNewPrivs=1 impede ganhos de privilégio associados a execve segundo o mecanismo documentado; não significa que todo o processo esteja sem privilégios ou que impeça todas as formas de alteração de credenciais.

5. Compatibilidade e evidência para RUN

Confirma caminhos de escrita e permissões com o dono da aplicação antes de tornar a raiz imutável. Um erro EROFS pode mostrar que o controlo funciona e que o contrato de execução está incompleto. Mantém limites de CPU, memória e processos no handover, distinguindo configuração inspecionada de um ensaio de carga que não foi feito. Para Secure Workload, verifica versão de agente, plataforma suportada e captura por interface; um problema eBPF pode retirar visibilidade sem eliminar comunicações. O ensaio local não instalou esse agente. Resumo: relaciona cada controlo com a propriedade que realmente mede, preserva requisitos da aplicação e não transforma ausência de telemetria em prova de silêncio.

"""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().splitlines()if 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))
NA PRÁTICA

Mesmo UID 65532: /tmp gravável no baseline; EROFS na raiz restrita; /scratch gravável, mas descartado após stop/start.

Armadilhas comuns

Non-root como imutabilidade; noexec como proibição de intérpretes; tmpfs como persistência ou apagamento seguro; limites configurados como ensaio de carga.

Tópicos relacionados: DevSecOps · Capacidades Linux · Observabilidade

Leva esta ideia contigo

As restrições devem ter efeito demonstrável e um contrato de aplicação compatível.

Criar conta

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

CCNP® e Cisco® são marcas registadas da Cisco Systems, Inc. e/ou das suas afiliadas. A dr.pt é uma plataforma de preparação independente e não está afiliada, associada, patrocinada, autorizada nem aprovada por Cisco. Os conteúdos e as perguntas são originais, não são perguntas oficiais de exame, e concluir os nossos testes não atribui nem garante qualquer certificação. Os nomes são usados apenas para identificar o tema. Todas as outras marcas pertencem aos respetivos titulares.