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:
- A fronteira existe.
orchestratoreprobenunca podem carregar o mesmo rótulo de confiança, everdict_authoritynunca pode serprobe. Um plano em que os dois lados sãotrustednão tem fronteira nenhuma a impor. - A probe não escreve onde o orquestrador lê. Nenhuma raiz gravável
declarada pode ser nem conter
orchestrator_rootoureport_root. É por essa regra — e não por uma igualdade de string — queprobe_writable_roots: ["/tmp"]é recusado:/tmpnão nomeia nenhuma das duas raízes, mas contém as duas. - 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'