Part 1 of #11 (does not close it; part 2 adds the resource caps). Closes the “strict SSH host-key verification” and “validate every job field” items.
What was wrong
client.SSHCommand built an ssh line with no host-key policy, so a runner
trusted whatever host answered. That covers the control channel (the job
payload), the LFS transfer, and — through GIT_SSH_COMMAND — git’s own
clone/fetch/push.
The runner also acted on every field of a leased job without re-checking it.
The server validates a workflow at push time, but the runner is the party that
turns the payload into filepath.Joins, a --volume, a site deploy and a
script, and the channel is the trust boundary the threat model names.
What this does
Host keys. client.HostKeys is a process policy: strict against
known_hosts by default, --known-hosts <path> to name a file,
VOLE_KNOWN_HOSTS for commands without a flag, and an explicit
--insecure-host-key / VOLE_INSECURE_HOST_KEY override that warns when used.
The options go before any configured ones, because ssh takes the first value
for an option, so a GIT_SSH_COMMAND=... StrictHostKeyChecking=no in the
environment cannot loosen the policy. GitSSHCommand() applies the same policy
to git’s children, which is how the clone, the record push and the job stream all
verify the same host. client/remote_test.go pins all of it.
The test suite uses the explicit override once, in TestMain, instead of every
fixture drifting toward trust-everything.
Job validation. ci/agent/validate.go re-checks a leased job before the
checkout, the cache, the container or the artifact patterns exist: run id,
repository name, attempt, commit id, ref (branch/tag, with git’s component
rules), branch, tag, workflow and job names, image or file: definition plus
its pinned digest, step count/names/scripts/timeouts, artifact and release
artifact patterns, pages path and ref, and the environment. A refusal stops the
runner with the reason rather than looping on a payload that will not improve.
Shared rules. ci/validate.go now owns the artifact and pages path rules and
the ref/object-id/env predicates; ci/workflow.go calls them instead of keeping
its own copy, so the push-time refusal and the runner-side refusal cannot
disagree. Validation also gains the checks the runner needed anyway: NUL and
control characters in a pages path, and : before a path becomes a --volume
argument.
Notes
--insecure-host-keyis required for the integration fixtures and is set in the test package’sTestMain. It is a real behaviour change for operators: existing runners needssh-keyscaninto known_hosts or--known-hosts.docs/runner.mdhas a new “Host key verification” section with the command.- An operator’s own
GIT_SSH_COMMANDstill supplies the program and-ikey; only the verification options are forced. That is the intended split: they own the key, the instance’s identity is not negotiable. - The pages/artifact validators are shared, but the server still composes the
messages with
jobs.%s...context; the wording is now generated in one place.
Verification
gofmt,go vet ./...,go test -race ./...clean../vole workflow checkand./vole workflow run verifypass.
Tests
client: strict by default,--known-hosts, the environment’s=nolosing to the policy, the insecure override, env parsing, andGitSSHCommandkeeping the operator’s program and key.ci: refs (namespace,..,//,.lock, control characters), object ids (40/64 hex, uppercase refused), artifact patterns and pages paths.ci/agent: a valid job passes; a table of 22 mutations is each refused with the offending field named, includingrepo=../../etc,pages.path=../../etc,artifact=../secret, a non-commit SHA, a ref with.., a tag with a newline, an envelope with a newline, an image reference that starts with-, and an image-file without a digest.