Pular para conteúdo

AUR-410 — Perfil de conformance OCI (oci-conformance-v1)

Documento histórico do board. Pode descrever componentes removidos; para uso atual, consulte o guia do produto.

Este documento descreve a entrada oci-conformance-v1 publicada no registry canônico de profiles. O card publica somente documentos: nenhum engine é invocado, nenhuma probe é criada, nenhum container é materializado, nenhum relatório de conformance é emitido, e o runner permanece fail-closed para esta chave até um card de implementação dependente habilitá-la explicitamente.

O objeto deste profile é uma fronteira, não uma execução: ele separa o orquestrador confiável das probes não confiáveis e fixa imagens, engines suportados, zero mounts, rootfs read-only, user, capabilities, rede e resource bounds em bytes versionados.

Artefatos

Artefato Caminho
Schema .board/schemas/oci-conformance-profile.schema.json
Profile .board/oci/profiles/oci-conformance-v1.json
Lock .board/locks/oci/oci-conformance-v1.lock.json
Registry .board/oci/profiles/registry.v1.json

O registry passa a publicar exatamente dez chaves, em ordem canônica crescente: bootstrap-readonly-v1, docs-tool-offline-v1, fake-provider-v1, fake-scm-offline-v1, go-git-offline-v1, go-unit-offline-v1, oci-conformance-v1, parser-worker-v1, registry-v1, sqlite-offline-v1. Nenhuma chave existente muda de digest, schema ou lock.

O índice foi determinado por LC_ALL=C sort sobre o conjunto de dez chaves, e não por estimativa. Em ordem lexicográfica C, oci- ordena depois de go-unit- — g (0x67) precede o (0x6F) no primeiro byte que difere — e antes de parser- — o (0x6F) precede p (0x70). A nova chave ocupa portanto o índice 6 (base zero), e as três chaves seguintes (parser-worker-v1, registry-v1, sqlite-offline-v1) deslocam-se uma posição à direita. Nenhuma chave sai da lista, nenhuma aridade exata vira mínimo e nenhuma igualdade vira desigualdade: cada chave continua asserida no único índice que a ordem canônica lhe dá.

Imagem pinada

O lock fixa Go e Bash na imagem de bootstrap já presente localmente, por digest imutável:

aurum-bootstrap-go-bash@sha256:503ac356fa6bca4bad56fade87b5479ff371bd446ffb9c7db91f211323c7c73e

A imagem foi escolhida por prova, não por conveniência. Sob --pull=never --network=none --read-only --user=65534:65534 --cap-drop=ALL (mais --security-opt=no-new-privileges) os dois probes locais respondem:

Imagem Bash Go CLI de engine Ferramentas de rede
aurum-bootstrap-go-bash@sha256:503ac356… 5.2.26 go1.21.13 docker, podman, nerdctl, ctr, crictl, runc, buildah, skopeo, containerd, dockerd, conmon, crun, umoci ausentes; nenhum socket de engine em /var/run/docker.sock, /run/docker.sock ou /run/podman/podman.sock curl, ssh, socat, telnet ausentes; wget, nc e ping presentes, e os três são applets do BusyBox 1.36.1 (/bin/busybox); uma única interface, lo
golang@sha256:4746d264… (imagem de go-git-offline-v1) presente presente os mesmos oito CLIs de engine ausentes curl, ssh, git e wget presentes

A afirmação precisa é esta: nenhuma das duas imagens locais carrega um CLI de engine, e portanto a escolha entre elas não se justifica por isso. O que a justifica é o resto da superfície. A imagem de bootstrap carrega Go e Bash — os dois runtimes que o command declarado e o próprio runner exigem — e não carrega curl, ssh nem git; a imagem golang carrega os três. Para um profile cuja fronteira inteira é "a probe não alcança nada", o runtime correto é o que tem menos superfície alcançável, não o que tem mais. go-git-offline-v1 continua sendo a chave dona daquele outro runtime; este profile é outra chave, com outro lock, e não pode tomá-lo emprestado.

Sobre wget e nc: eles existem na imagem de bootstrap, como applets do BusyBox. O plano não afirma a ausência dessas ferramentas; afirma que elas não alcançam nada, porque network: "none" deixa uma única interface (lo), egress: "denied", HTTP_PROXY e HTTPS_PROXY são vazios e NO_PROXY é *. A negação aqui é estrutural (a rede), não lexical (o binário).

Plano declarado

A fronteira orquestrador/probe

Esta é a razão de ser do profile, e é o único ponto em que ele difere dos nove anteriores em espécie e não apenas em domínio.

Campo Valor O que ele nega
orchestrator trusted o lado que decide
probe untrusted o lado que é medido
probe_output untrusted-observation saída de probe como resultado
probe_verdict_authority denied veredito emitido pela probe
verdict_authority orchestrator veredito emitido por qualquer outro
probe_writable_roots ["/tmp/aurum-oci-probes"] escrita fora da raiz da probe
orchestrator_root /tmp/aurum-oci-orchestrator orquestrador no checkout
report_root /tmp/aurum-oci-reports relatório no checkout
report_mode ephemeral relatório persistente

Três regras estruturais sustentam essa tabela independentemente das constantes acima, de modo que relaxar as constantes de forma coerente nos três arquivos ainda não abre a fronteira:

  1. A fronteira existe. orchestrator e probe nunca podem carregar o mesmo rótulo de confiança, e verdict_authority nunca pode ser probe. Um plano em que os dois lados são trusted não tem fronteira nenhuma a impor.
  2. A probe não escreve onde o orquestrador lê. Nenhuma raiz gravável declarada pode ser nem conter orchestrator_root ou report_root. É por essa regra — e não por uma igualdade de string — que probe_writable_roots: ["/tmp"] é recusado: /tmp não nomeia nenhuma das duas raízes, mas contém as duas.
  3. As três raízes são disjuntas e não aninhadas. Uma raiz de probe sob a raiz do orquestrador, ou uma raiz de relatório sob a raiz da probe, colocaria o lado não confiável dentro da árvore do lado confiável.

A separação não é apenas declarada: ela é executada. ResolveConformanceAUR410 recebe a saída da probe como argumento, a trunca em max_probe_bytes, publica o digest dessa observação e calcula o veredito somente a partir dos documentos registrados. tests/unit/AUR-410.go roda o orquestrador uma vez por observação hostil — um JSON "result": "valid", um "proved": true com "review": "approved", uma linha --- PASS/ok, um código de rejeição e um payload maior que o próprio limite — e exige que o veredito seja idêntico byte a byte nas sete execuções, enquanto o digest da observação registra seis valores distintos. Se a observação fosse ignorada por completo, os seis digests colapsariam em um e a invariância não provaria nada. A mesma exigência vale sob cada mutante: uma observação hostil não resgata um plano rejeitado.

Engines suportados

supported_engines é ["docker", "podman"], comparado elemento a elemento, e unsupported_engine é denied. EngineSupportedAUR410 é a função que decide, e ela consulta a lista publicada somente depois que o nome passou pela expressão fechada \A(docker|podman)\z. Isso torna a lista incapaz de admitir o que a expressão nega: nerdctl, ctr, crictl, runc, buildah, Docker, DOCKER, " docker", "docker ", "docker\n", "docker\npodman", "docker;podman", /usr/bin/docker, podman-remote e a string vazia são todos recusados, e o teste executa os quinze casos.

Zero invocações de engine, declaradas e medidas

engine_invocations é 0. Isso é uma afirmação feita por um documento JSON, e o acceptance não a aceita como prova: ele instala shims para docker, podman, nerdctl, ctr, crictl, runc, buildah, skopeo, docker-compose e podman-remote à frente do PATH, cada um registrando o próprio nome em um ledger e saindo com 70. Qualquer tentativa de alcançar um engine — das camadas Go, do programa E2E, da regressão de compatibilidade ou de um mutante — fica escrita.

O ledger é provado load-bearing antes de ser usado como prova de ausência: o shim é invocado uma vez de propósito e precisa aparecer (engine-ledger-inert se não aparecer). Só então o ledger é zerado, e ao final exige-se engine-ledger-nonzero == 0. A camada Go faz a mesma leitura e recusa o plano se o valor declarado e o medido divergirem. Declarado 0, medido 0.

Contenção

Campo Valor
network none
user 65534:65534
cap_drop / cap_add ALL / none
mounts / devices / sockets none
privileged false
read_only_rootfs true
no_new_privileges true
checkout_readonly true
pull never
tmpfs rw,nosuid,nodev
engine_socket / engine_api / host_engine denied
egress / host_checkout / host_filesystem / subprocess denied

Limites

Todo limite é uma constante exata, nunca um mínimo: um valor maior que o declarado é tão inseguro quanto um valor zero, porque o relatório e o corpus de probes têm de caber dentro do próprio tmpfs e do próprio limite de entrada.

Limite Valor
timeout_seconds 60
probe_timeout_seconds 5
memory_mb 256
cpu_millis 1000
pids_limit 128
tmpfs_mb 128
stdout_limit_bytes / stderr_limit_bytes 65536
max_input_files 10000
max_input_bytes 67108864
max_probes 1024
max_probe_bytes 65536
max_report_bytes 1048576

Independentemente das constantes, o loader exige max_report_bytes <= tmpfs_mb * 1024 * 1024, max_probes <= max_input_files, max_probes * max_probe_bytes <= max_input_bytes e 0 < probe_timeout_seconds <= timeout_seconds. Com os valores publicados, 1024 * 65536 é exatamente max_input_bytes, de modo que max_probes: 2048 estoura o próprio limite declarado e é recusado por essa relação e não por uma constante.

Ambiente

Os quinze valores de ambiente fecham por valor todo canal por onde um cliente de engine seria endereçado: DOCKER_HOST e CONTAINER_HOST vazios (nenhum endpoint), DOCKER_CONFIG em /dev/null (nenhum credential store, nenhum contexto), DOCKER_API_VERSION vazio, HTTP_PROXY/HTTPS_PROXY vazios, NO_PROXY igual a *, CGO_ENABLED igual a 0 e GOPROXY/GOSUMDB desligados. Cada um é fixado por constante, de modo que reabrir qualquer um deles é um mutante provado.

Raízes ancoradas

orchestrator_root, probe_root e report_root são casados por expressões ancoradas em \A e \z — início e fim de texto, nunca de linha. São recusados, e cada caso é um mutante executado: o checkout (/workspace), uma travessia que começa dentro da raiz limitada e sai dela, uma barra final, um irmão que apenas compartilha o prefixo, um valor com quebra de linha embutida, um valor com quebra de linha ao final, um espaço à esquerda, a grafia em maiúsculas, um homóglifo Unicode (o o de probes trocado pelo о cirílico, U+043E) e a string vazia.

Comandos permitidos

command é ["go", "test", "-mod=readonly", "./..."], comparado elemento a elemento. O acceptance deste card, porém, roda sob bootstrap-readonly-v1 e é validação pura de documentos: ele não invoca engine, não abre socket, não cria subprocesso de engine e não materializa container algum.

Códigos de rejeição

Código Quando
schema-missing / profile-missing / lock-missing documento declarado ausente ou não regular
schema-invalid schema relaxado, conjunto de chaves aberto, chave obrigatória removida
digest-invalid probe_image_set não content-addressed, digest fora de forma
mutable-input imagem por tag mutável ou lock que não vincula a esta chave
lock-digest-mismatch / schema-digest-mismatch digest publicado não reproduz os bytes
duplicate-profile a mesma chave publicada duas vezes
order-invalid ordem canônica quebrada
profile-unregistered chave desconhecida, conjunto de chaves diferente do registrado
credential-present forma de credencial em qualquer valor de ambiente
unsafe-plan qualquer violação de contenção, fronteira, engine, raiz ou limite

Mutantes provados

O acceptance executa 67 mutações, cada uma alterando bytes em disco e exigindo o código correspondente. Além das três declaradas no TaskSpec (duplicate-profile, mutable-input, unsafe-plan), estão provadas as nove enumeradas em AC-001 — root-user, privileged-enabled, socket-enabled, mount-enabled, device-enabled, capability-added, egress-allowed, writable-rootfs e limite ausente, nas duas formas: chave removida (missing-pids-limit, missing-probe-timeout, missing-supported-engines → schema-invalid) e limite anulado (unbounded-report, unbounded-probe-bytes, unbounded-probe-timeout, unlimited-tmpfs → unsafe-plan).

Os ataques específicos desta fronteira também são mutantes executados: probe-output-authoritative, probe-output-trusted, probe-verdict-advisory, verdict-authority-probe, probe-trusted, orchestrator-untrusted, probe-writes-orchestrator-root, probe-writes-report-root, probe-writes-parent-root, probe-writable-roots-empty, engine-list-widened, engine-list-unsupported, engine-list-empty, engine-list-duplicated, unsupported-engine-allowed, engine-invoked, engine-socket-allowed, engine-api-allowed, host-engine-allowed, engine-socket-environment, container-host-environment e engine-config-environment.

Loaders vizinhos

Os nove loaders já entregues fecham o próprio allowlist de chaves sobre o conjunto registrado, e o loader de AUR-403 ainda assere o comprimento do registry e a ordem posicional. Registrar uma décima chave quebra as regressões de AUR-402 a AUR-409 de fora deste card, a menos que elas a aprendam no mesmo commit — e é exatamente por isso que os oito arquivos estão em paths. A aridade exata de tests/contracts/AUR-404.go sobe de nove para dez pela mesma razão.

Nenhum deles lê os documentos de conformance: cada um verifica apenas os caminhos declarados e a forma dos digests, de modo que registrar o profile de conformance não pode silenciosamente re-apontar o plano de unidade Go, o de Git, o de provider falso, o do parser, o do SQLite, o de documentação ou o de SCM falso. A aridade total do registry é asserida em tests/unit/AUR-410.go, que este card possui, e deliberadamente não em tests/contracts/AUR-410.go: este card é dono da chave oci-conformance-v1, não do tamanho do registry.

Diagnóstico offline

# a chave está publicada e em ordem?
grep -Fx '    "key": "oci-conformance-v1",' .board/oci/profiles/registry.v1.json
python3 -c 'import json;d=json.load(open(".board/oci/profiles/registry.v1.json"));k=[e["key"] for e in d["profiles"]];print(len(k), k==sorted(k), k.index("oci-conformance-v1"))'

# os digests do registry reproduzem os bytes?
sha256sum .board/schemas/oci-conformance-profile.schema.json .board/locks/oci/oci-conformance-v1.lock.json
printf '%s' "$(python3 -c 'import json;print(json.load(open(".board/locks/oci/oci-conformance-v1.lock.json"))["image"])')" | sha256sum

# o runtime da imagem pinada ainda satisfaz o plano puro Go sem CLI de engine?
docker run --rm --pull=never --network=none --read-only --user=65534:65534 \
  --cap-drop=ALL --security-opt=no-new-privileges \
  --entrypoint /bin/bash \
  aurum-bootstrap-go-bash@sha256:503ac356fa6bca4bad56fade87b5479ff371bd446ffb9c7db91f211323c7c73e \
  -c 'go version; for t in docker podman nerdctl ctr crictl runc buildah skopeo; do command -v $t || echo "ABSENT $t"; done; awk "NR>2{print \$1}" /proc/net/dev'