Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

Verify the runner's host and re-check the job it is given

f5456470d22fb7ca06197a8856ed0da7fc56fdd2 · Keenan Nemetz · 12d ago · parent edf9db6e

Verify the runner's host and re-check the job it is given

A runner executes repository code, so the instance it talks to decides what
runs on it. The client built ssh command lines with no host-key policy: a
man in the middle, or a loose known_hosts, could answer in the instance's
place and feed a runner any job it liked.

Host keys are now verified against known_hosts by default, for the
runner's control channel, for git's own clone/fetch/push and for the LFS
transfer. `--known-hosts` names a file instead of the default,
VOLE_KNOWN_HOSTS does the same for every command, and
`--insecure-host-key` / VOLE_INSECURE_HOST_KEY is the explicit override
for tests and throwaway instances, with a warning when it is used. The
options are placed before any configured ones because ssh takes the first
value for an option, so GIT_SSH_COMMAND or VOLE_SSH_COMMAND cannot
quietly turn the check off. The test package opts out once, in TestMain,
rather than every fixture learning to trust everything.

The runner also re-checks the job it leases before acting on it. The
server validated the workflow at push time, but the runner is the party
that turns the payload into paths, container arguments and scripts. Run
id, repository, attempt, commit, ref, branch, tag, workflow and job names,
image (or image-file plus its pinned digest), steps, artifact patterns,
pages path and environment are all validated; a malformed job stops the
runner with the reason instead of being built from.

The artifact and pages path rules move into ci/validate.go, so the
push-time check and the runner use one implementation rather than two
that can drift. Joining them also closes the gap the runner-side check
was for: `..`, absolute paths, `**`, globs where a directory belongs, and
control characters are refused identically on both sides.

Tests: strict-by-default and known_hosts options, the environment losing
to the explicit policy, the insecure override, and every job field
refused when it climbs out, is the wrong shape, or is missing.

client/remote.go +68−1

@@ -9,9 +9,75 @@ import (
9 9 "strings"
10 10 )
11 11
12 // HostKeys is how this process verifies the SSH host key of the instance it talks
13 // to. The zero value is strict: ssh checks the key against the caller's known_hosts
14 // and refuses a host it has not been told to trust. A loose known_hosts, or a
15 // command line that disables the check, is what lets a man in the middle feed a
16 // runner jobs — the runner executes repository code, so the host it trusts is a
17 // security boundary, not a convenience.
18 type HostKeys struct {
19 // KnownHosts is a known_hosts file to check against. Empty uses ssh's own
20 // defaults (~/.ssh/known_hosts and the system file).
21 KnownHosts string
22 // Insecure turns verification off. It exists for tests and for a disposable
23 // development instance, and a caller that sets it is saying out loud that it
24 // does not care which host answers.
25 Insecure bool
26 }
27
28 var hostKeys HostKeys
29
30 // SetHostKeys replaces the host-key policy for this process. It is expected to be
31 // called once, before any command that talks to an instance.
32 func SetHostKeys(h HostKeys) { hostKeys = h }
33
34 // HostKeysFromEnv reads VOLE_KNOWN_HOSTS and VOLE_INSECURE_HOST_KEY, which are how
35 // a command that has no flag of its own opts into a policy.
36 func HostKeysFromEnv() HostKeys {
37 h := HostKeys{KnownHosts: strings.TrimSpace(os.Getenv("VOLE_KNOWN_HOSTS"))}
38 switch strings.ToLower(strings.TrimSpace(os.Getenv("VOLE_INSECURE_HOST_KEY"))) {
39 case "1", "true", "yes":
40 h.Insecure = true
41 }
42 return h
43 }
44
45 // HostKeyArgs are the ssh options that apply the current policy.
46 func HostKeyArgs() []string {
47 if hostKeys.Insecure {
48 return []string{"-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null"}
49 }
50 args := []string{"-o", "StrictHostKeyChecking=yes"}
51 if hostKeys.KnownHosts != "" {
52 args = append(args, "-o", "UserKnownHostsFile="+hostKeys.KnownHosts)
53 }
54 return args
55 }
56
57 // GitSSHCommand is the GIT_SSH_COMMAND that gives git's own ssh calls the same
58 // policy, keeping whatever program and key options the operator configured. A
59 // command sets it for its children, which is how a checkout, a push and an
60 // ssh-keyscan in a workflow all verify the same host.
61 func GitSSHCommand() string {
62 base := []string{"ssh"}
63 if v := strings.TrimSpace(os.Getenv("GIT_SSH_COMMAND")); v != "" {
64 base = strings.Fields(v)
65 } else if v := strings.TrimSpace(os.Getenv("VOLE_SSH_COMMAND")); v != "" {
66 base = strings.Fields(v)
67 }
68 parts := append([]string{base[0]}, HostKeyArgs()...)
69 parts = append(parts, base[1:]...)
70 return strings.Join(parts, " ")
71 }
72
12 73 // SSHCommand builds an ssh invocation for host, honouring GIT_SSH_COMMAND (which
13 74 // git itself uses, and which a runner therefore already sets) and then
14 75 // VOLE_SSH_COMMAND. The returned command's stderr is left for the caller.
76 //
77 // The host-key options go in before the configured ones: ssh takes the first value
78 // it is given for an option, so an environment variable cannot loosen the check by
79 // adding StrictHostKeyChecking=no after it. The explicit HostKeys policy is the only
80 // way to do that.
15 81 func SSHCommand(host, port string, args ...string) *exec.Cmd {
16 82 base := []string{"ssh"}
17 83 if v := strings.TrimSpace(os.Getenv("GIT_SSH_COMMAND")); v != "" {
@@ -19,7 +85,8 @@ func SSHCommand(host, port string, args ...string) *exec.Cmd {
19 85 } else if v := strings.TrimSpace(os.Getenv("VOLE_SSH_COMMAND")); v != "" {
20 86 base = strings.Fields(v)
21 87 }
22 argv := append([]string{}, base[1:]...)
88 argv := append([]string{}, HostKeyArgs()...)
89 argv = append(argv, base[1:]...)
23 90 argv = append(argv, "-T", "-o", "BatchMode=yes")
24 91 if port != "" {
25 92 argv = append(argv, "-p", port)