refactor: adapter 구현중..
This commit is contained in:
@@ -10,6 +10,39 @@ checking, the typed contract checker, and this workflow drift check. The Gitea
|
||||
adapter runs each gate as an independent matrix check with full fan-out and no
|
||||
soft-fail wiring.
|
||||
|
||||
Contract loading validates every registered command entry against the
|
||||
authoritative root package-script graph and lifecycle/install policy before the
|
||||
runner enters its execution callback. Foreign cwd/workspace script dispatch,
|
||||
argument-sensitive dispatchers, and ineffective lifecycle suppression therefore
|
||||
fail preflight even when the later `check:ci` command would never run.
|
||||
For npm, that root-only boundary also parses options following an explicit
|
||||
`run`/`run-script` target or implicit `test`/`start`/`stop`/`restart` target:
|
||||
`--workspace`, `-w`, `--workspaces`, and `--prefix` are foreign manifest scope
|
||||
and are rejected before execution. The first literal `--` ends npm option
|
||||
parsing, so later tokens are ordinary script arguments. pnpm and Yarn differ:
|
||||
their options following the selected script name are forwarded to that script;
|
||||
their cwd/workspace selectors are rejected only where the manager consumes
|
||||
them before script selection.
|
||||
|
||||
The npm graph includes existing root-manifest `pre<script>` and `post<script>`
|
||||
hooks around every explicit `run`/`run-script` dependency and implicit
|
||||
`start`/`stop`/`restart`/`test` dependency. Hooks are omitted only when ordered
|
||||
npm options establish an unambiguous effective `--ignore-scripts` value before
|
||||
the first literal `--`; bare and explicitly true forms omit hooks, while false,
|
||||
negative, contradictory, malformed, and post-delimiter forms keep traversal or
|
||||
fail closed. Case-insensitive `npm_config_workspace`, `npm_config_workspaces`,
|
||||
`npm_config_prefix`, `npm_config_userconfig`, and `npm_config_globalconfig`
|
||||
assignments are rejected whenever the tokenized command invokes npm. Direct
|
||||
assignments, static paths whose basename is `env`, exact `command`/`exec`
|
||||
prefix chains, prior exports, and `set -a` assignments share one prefix grammar.
|
||||
Only modeled non-scope `env -i`, `env -u`, `env --unset`, and `env --` forms are
|
||||
allowed; cwd-changing or unknown options fail closed. Dynamic assignment names,
|
||||
unmodeled environment mutation, npm `--userconfig`/`--globalconfig`, and
|
||||
unquoted pathname expansion before the npm argument delimiter are rejected.
|
||||
The gate runner checks the same inherited environment names before loading the
|
||||
contract or entering the execution callback, so they cannot reach a child gate
|
||||
process.
|
||||
|
||||
The dependency graph is:
|
||||
|
||||
```text
|
||||
@@ -52,19 +85,98 @@ digest가 일치할 때만 report를 업로드한다. Promotion은 같은 archiv
|
||||
별도 경로로 내려받고 SHA/member 검증을 마친 뒤 격리된 root에 추출하여 local
|
||||
evidence를 read-only로 다시 계산하고 Ed25519 signature/digest를 확인한다.
|
||||
Promotion job에는 build/rebuild command가 없으며 검증한 archive 자체를 변경 없이
|
||||
그대로 승격한다.
|
||||
그대로 승격한다. Local evidence 관계 검증에 필요한 policy와 verifier source도
|
||||
archive member로 고정되며, checkout 밖의 격리된 cwd에서도 archive path와 기대
|
||||
digest만으로 candidate-internal check를 재계산하고 archived secret-scan
|
||||
policy/rule/SARIF/zero-finding/digest 관계를 검증한다. Archive에 없는 checkout
|
||||
source를 다시 scan했다고 주장하지 않는다.
|
||||
|
||||
Provider baseline은 Gitea 1.26.4 이상과 Gitea Runner 1.0.0 이상이다. 이
|
||||
workflow의 provider job은 Linux runner에서 실행 권한이 있는
|
||||
`/usr/bin/bwrap`를 필수로 요구하며, trusted `process.execPath`를 sandbox 안의
|
||||
`/tmp/node`에 read-only bind한다. Provider command는 bubblewrap 안에서
|
||||
`/bin/sh -eu -c`로 비대화식 실행되고 30분 안에 종료되어야 한다. Sandbox는
|
||||
workspace를 read-only로 bind하고 `.git`을 가리며, 별도의 `untrusted`
|
||||
raw-evidence 하위 디렉터리만 writable로 노출한다. 따라서 command는 전달된
|
||||
candidate/environment 값을 읽고 지정된 raw report 하나만 기록해야 하며,
|
||||
workspace 수정, host home/toolcache 접근, sealed evidence 직접 기록에 의존하면
|
||||
안 된다. Supervisor는 provider 종류에 해당하는 credential prefix와 제한된
|
||||
환경만 전달하고, sandbox 또는 출력 경계를 만들 수 없으면 fail closed한다.
|
||||
Provider baseline은 Gitea 1.26.4 이상과 Gitea Runner 1.0.0 이상이다. Linux
|
||||
runner에는 실행 가능한 `/usr/bin/bwrap`, `/usr/bin/prlimit`,
|
||||
`/usr/bin/systemd-run`, `/usr/bin/systemctl`, `bwrap --size` 지원, active user
|
||||
bus, systemd user manager 254 이상, unified cgroup v2와 delegated
|
||||
memory/pids/CPU controller가 모두 필요하다. 실행 파일, user manager/version,
|
||||
trust/archive/path 같은 host-side preflight 실패는 raw report 생성 전에 차단된다.
|
||||
`bwrap --size` 수용 여부와 실제 delegated controller/limit 값은 owned raw inode를
|
||||
만든 뒤 scope 안에서만 확정할 수 있으며, 이 단계의 실패는 해당 inode를
|
||||
identity-bound cleanup하고 fail closed한다.
|
||||
|
||||
각 provider는 고유한 collected user scope에서 실행된다. Supervisor는 실행 전에
|
||||
실제 cgroup membership과 `memory.max=1073741824`, `memory.swap.max=0`,
|
||||
`pids.max=64`, `cpu.max="100000 100000"`을 확인한다. 내부 process에는 core 0,
|
||||
file-size 8,388,607 bytes, open FD 64, CPU 1,200초 상한도 적용된다.
|
||||
같은 UID 전체에 합산되는 `RLIMIT_NPROC`로 provider별 32개를 보장한다고 주장하지
|
||||
않으며 aggregate PID authority는 cgroup `TasksMax=64`다.
|
||||
Bubblewrap는 network namespace를 분리해 완전 offline으로 실행하고 workspace,
|
||||
verified candidate, `.git` mask, `/tmp`, `/etc`, `/proc`, `/dev`를 read-only로
|
||||
유지한다. Archive 검증·추출과 sandbox/trust preflight가 끝난 뒤 supervisor가
|
||||
생성하고 inode를 고정한 정확한 raw report 파일 하나만 read-write bind된다.
|
||||
주변 `untrusted` directory 전체는 writable이 아니다. 실행 전 또는 provider
|
||||
실패 시, 그리고 성공적으로 sealed evidence를 게시한 뒤에도 supervisor가 소유한
|
||||
inode만 atomic quarantine을 거쳐 제거하므로 빈 stale report 없이 재시도할 수 있다.
|
||||
|
||||
Provider command와 provider-prefixed environment는 bounded length-prefixed bwrap vector로
|
||||
`systemd-run` stdin에 전달되어 supervisor/systemd/bwrap wrapper argv나 unit
|
||||
metadata에 노출되지 않는다. 단, 최종 provider executable의 일반 argv는 같은
|
||||
UID의 process inspection에 보일 수 있으므로 command 문자열과 인자에 token,
|
||||
password, private-key material을 넣으면 안 된다. Credential은 반드시 해당 종류의
|
||||
`VULNERABILITY_PROVIDER_*` 또는 `PROVENANCE_PROVIDER_*` environment로만 전달하고
|
||||
`*_COMMAND`에는 넣지 않는다. 정상 provider 종료까지 같은 stdin을 parent-liveness
|
||||
pipe로 열어 두며 supervisor hard death의 EOF를 받은 in-scope wrapper는 provider
|
||||
process group 전체를 종료하고 dev/inode가 일치하는 raw report만 정리한다.
|
||||
|
||||
별도의 trusted guardian child는 provider scope 밖에서 filesystem transaction 전체를
|
||||
소유한다. Client는 spawn 전에 canonical raw/evidence directory를
|
||||
`O_DIRECTORY|O_NOFOLLOW`로 열고 identity를 확인한 뒤 provider kind와 nonce에서
|
||||
canonical raw/final 및 nonce-private raw-staging/sealed-temp exact leaf를 확정한다.
|
||||
Client가 두 private file을 `O_RDWR|O_CREAT|O_EXCL|O_NOFOLLOW` mode `0600`으로
|
||||
미리 할당하고 dev/inode를 기록한다. Directory descriptor는 guardian fd 3/4,
|
||||
private file descriptor는 fd 5/6으로 상속되며 guardian argv에는 Node와 helper
|
||||
경로만 있다. 최초 canonical v2 frame에는 provider kind, absolute deadline,
|
||||
32-byte nonce만 전달한다.
|
||||
|
||||
Guardian bootstrap은 procfs link를 읽기 전에 fd 5/6을 fstat한다. Procfs pathname은
|
||||
canonical directory의 exact direct-child grammar를 만족하고 descriptor-relative
|
||||
lstat이 이미 확보한 fd identity/type/mode/size/link count와 일치할 때만 해당
|
||||
identity의 cleanup alias가 된다. Guard frame 검증 뒤 raw staging을 fixed raw leaf에
|
||||
no-overwrite hard link하고 두 alias의 link count 2를 확인한 다음 private alias를
|
||||
제거하고 raw directory를 sync한다. Canonical raw가 같은 identity와 link count 1로
|
||||
남은 뒤에만 READY를 응답한다. READY 전 종료 시 client는 pre-spawn raw identity로
|
||||
raw staging/canonical을, sealed identity로 temp/final을 각각 확인해 일치하는 alias만
|
||||
정리한다. 현재 canonical pathname을 새 ownership identity로 승격하지 않으므로 외부
|
||||
file과 concurrent same-kind winner를 보존하면서 같은 workspace를 즉시 재시도할 수
|
||||
있다. Supervisor는 READY identity와 canonical target도 정확히 확인한다.
|
||||
|
||||
검증된 JSON bytes는 pinned temp inode에만 기록하고 `0400` 적용과 file sync를 마친다.
|
||||
Guardian은 authenticated publish의 size/hash/identity를 재검증하고 같은 directory에서
|
||||
no-replace `link(temp, final)`, temp unlink, directory sync를 수행한 뒤 PUBLISHED를
|
||||
응답한다. `GITHUB_OUTPUT` append 이후 commit은 raw inode를 제거하고
|
||||
`commitPending`으로만 전이하며, 깨끗한 control EOF까지 확인해야 PASS가 된다. 그 전의
|
||||
EOF, deadline, 잘못된 frame/nonce, commit 뒤 추가 byte 또는 guardian 조기 종료는
|
||||
identity가 일치하는 raw/temp/final을 모두 정리하고 fail closed한다. Provider wall
|
||||
timeout 30분에 post-processing 10분을 더한 guardian lease 상한은 40분이다.
|
||||
|
||||
Client가 private allocation 뒤 guardian spawn 전에 hard stop되면 empty mode-`0600`
|
||||
nonce-private leaf만 남을 수 있다. 기록되지 않은 pathname은 ownership 근거가 아니므로
|
||||
자동 sweeping하지 않는다. 이 private leaf는 fixed raw/final name을 점유하지 않아
|
||||
same-kind retry를 막지 않는다.
|
||||
|
||||
`GITHUB_OUTPUT`은 runner가 소유한 regular file이라는 실행기 계약을 전제로 한다.
|
||||
Sealing/output I/O의 OS-level cancellation을 보장하지 않는다. Provider scope가 이미
|
||||
수집된 뒤 guardian이 종료되면 scope-active latch는 늦은 kill을 시작하지 않고 lifecycle
|
||||
error만 기록한다. 이후 publish/commit의 awaited failure가 identity가 고정된
|
||||
raw/temp/final fallback을 모두 정리하고 fail closed한다.
|
||||
|
||||
Provider stdout/stderr는 credential을 포함할 수 있는 untrusted bytes이므로 CI log로
|
||||
재전송하거나 보관하지 않고, byte 수만 합산해 1 MiB 상한을 적용한다. Guardian의
|
||||
stderr/control fd가 닫혀 진단 출력이 `EPIPE`/`EBADF`가 되어도 cleanup 뒤 nonzero
|
||||
종료는 생략되지 않는다. Provider wall-clock 상한은 30분이다. Wall
|
||||
timeout, aggregate output 초과, parent-liveness loss, 실행 중 guardian loss는 provider process group을
|
||||
명시적으로 SIGKILL한다. 일반 command 실패와 FD/CPU RLIMIT 종료는 실제 exit/signal로
|
||||
systemd completion을 거치며, 모든 경로에서 wrapper 종료와 systemd unit/cgroup
|
||||
collection을 확인한다. Adapter는 사전에 배치된 offline data와 supervisor candidate binding만
|
||||
읽어 정확한 report inode에 기록해야 한다. Scope/cgroup limit drift, residual unit,
|
||||
workspace·host home/toolcache·sealed evidence 접근 의존성은 모두 blocking failure다.
|
||||
|
||||
Workflow가 실행하는 action은 `scripts/contracts/ci-gates.ts`의 단일 typed,
|
||||
runtime-frozen registry에서만 resolve된다. `uses:`에는 repository 별칭, tag,
|
||||
@@ -192,6 +304,9 @@ Repository variables required by higher tiers:
|
||||
`PROVENANCE_PUBLIC_KEY_PATH`, and `PROVENANCE_KEY_ID` for separately managed
|
||||
trusted Ed25519 verification material
|
||||
|
||||
두 provider role은 서로 다른 key ID뿐 아니라 canonical DER-SPKI public-key bytes도
|
||||
사용해야 한다. 동일 key를 서로 다른 ID로 재등록한 구성도 finalizer가 거절한다.
|
||||
|
||||
If any external provider command, report, trust path, or key ID is absent,
|
||||
promotion remains unavailable with `FAIL_UNVERIFIED`; there is no local
|
||||
generator/restore fallback.
|
||||
@@ -215,20 +330,51 @@ consumer도 artifact service나 transfer action을 신뢰 경계 밖으로 보
|
||||
manifest와 signed provider evidence에 바인딩된 digest를 다운로드 후 다시
|
||||
검증해야 한다. 현재 producer-side adjacency 자체는 consumer-side digest
|
||||
revalidation을 대신하지 않는다.
|
||||
Promotion job에는 job-level `if`가 없다. 기본 `needs` 성공 의미론으로 immutable
|
||||
build, vulnerability provider, provenance provider 세 job이 모두 성공해야 하며,
|
||||
`always()`나 `cancelled()`로 cancellation을 덮어쓰지 않는다. Bare `always()`는
|
||||
step cleanup에만 사용된다. 다만 cancellation 시 cleanup 실행 여부는 workflow
|
||||
정적 타입이나 단위 테스트로 증명하지 않았으며 runner/native smoke에서 확인해야
|
||||
하는 신뢰 경계다.
|
||||
Finalizer output은 `RUNNER_TEMP` 아래 random private directory이며 exact-five
|
||||
upload는 `${{ steps.finalize.outputs.staging_root }}`만 사용한다. 바로 다음
|
||||
`always()` cleanup은 finalizer의 token과 runner-temp device/inode를 모두
|
||||
요구한다. stable `.release/promoted-staging` directory를 만들거나 재사용하지
|
||||
않는다. exact five는 captured archive/report 두 개와 process 안에서 생성한
|
||||
provider/promotion verification v3 두 개이며 promotion record는 provider record,
|
||||
local assessment, report hashes와 run/source/candidate/nonces/key identities/
|
||||
trust-policy hash를 함께 bind한다. 이 descriptor-relative 정리는 ancestor 교체와 symlink leaf를
|
||||
fail-closed로 처리하지만 upload action의 same-UID pathname reopen 또는 atomic
|
||||
upload는 `${{ steps.finalize.outputs.staging_root }}` 아래 다음 다섯 canonical
|
||||
pathname만 사용한다: `release-candidate.tar.gz`, `vulnerability-report.json`,
|
||||
`provenance-attestation.json`, `provider-verification.json`,
|
||||
`promotion-verification.json`. 바로 다음
|
||||
`always()` cleanup은 staging path/token, runner-temp device/inode와 staging-leaf
|
||||
device/inode 여섯 output을 모두 요구한다. cleanup은 pin한 leaf descriptor에서
|
||||
exact-five name만 unlink하고 non-recursive `rmdir`만 사용하므로 교체된 directory나
|
||||
canary tree를 recursive 삭제하지 않는다. stable `.release/promoted-staging`
|
||||
directory를 만들거나 재사용하지 않는다. exact five는 captured archive 한 개,
|
||||
captured report 두 개와 process 안에서 생성한 provider/promotion verification v3
|
||||
두 개이며 promotion
|
||||
record는 provider record, local assessment, report hashes와 run/source/candidate/
|
||||
nonces/key identities/trust-policy hash 및 signed `secretScanAttestation`을 함께
|
||||
bind한다. 이 attestation은 PASS와 local-assessment/source-set/policy/SARIF/
|
||||
scan-input digest를 포함한다. Supervisor/finalizer는 captured archive에서 기대
|
||||
tuple을 유도해 exact equality를 확인하지만, 실제로 같은 source-set 전체를
|
||||
독립 스캔하고 forged empty SARIF에 서명하지 않을 책임은 trusted vulnerability
|
||||
provider에 있다. Staging은 restrictive
|
||||
umask와 무관하게 directory `0700`, file `0400`을 강제하고, 모든 write 뒤 live
|
||||
time으로 exact-five signature/freshness를 다시 확인한 뒤에만 output을 공개한다.
|
||||
Descriptor-relative 정리는 ancestor/leaf 교체와 symlink를 fail-closed로 처리하지만
|
||||
upload action의 same-UID pathname reopen 또는 atomic
|
||||
`renameat2` handoff를 보장하지 않는다. staging Gitea smoke/native adapter 확인
|
||||
전에는 그 경계를 닫았다고 보고하지 않는다. 실제 smoke는 exact-five
|
||||
upload-download와 success, validation failure, upload failure, cancellation 각각의
|
||||
cleanup을 관찰해야 한다. 현재 repository에는 native uploader나 `renameat2`
|
||||
보장이 없다.
|
||||
또한 portable Node의 `mkdir`와 최초 pathname `lstat`는 atomic하지 않다. 구현은
|
||||
mkdir 직후 metadata를 저장하고 이후 `O_DIRECTORY|O_NOFOLLOW` descriptor의
|
||||
device/inode와 비교한 뒤에만 permission을 바꾸지만, 최초 lstat보다 앞서 성공한
|
||||
malicious same-UID 교체는 native/privilege 경계로 남는다. 따라서 `RUNNER_TEMP`
|
||||
private `0700` ancestor와 exclusive single-tenant runner가 필수다.
|
||||
실패 cleanup도 created device/inode와 opened descriptor가 일치한 뒤에만 활성화된다.
|
||||
불일치 descriptor는 close만 수행하며 현재 visible replacement pathname은 unlink나
|
||||
`rmdir`하지 않는다. 공격자가 original directory를 다른 이름이나 parent 밖으로
|
||||
이동한 경우 portable Node parent scan으로 안전하게 회수할 수 없으므로, 공격자를
|
||||
배제한 trusted runner/native cleanup 또는 격리된 test fixture가 잔여 directory를
|
||||
후처리해야 한다.
|
||||
|
||||
Branch protection must mark each `FE-GATE-* / <name>` check required for its
|
||||
declared tier. This repository cannot configure server-side protection by
|
||||
|
||||
Reference in New Issue
Block a user