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.

Blame · ci/agent/credential.go

Line attribution on main · 75 line(s)

6e1fca51
Keenan Nemetz
6d ago
1 package agent
2
3 // The environment a job acting as a bot gets.
4 //
5 // The credential is a file in the run directory, and git and the `vole` CLI both
6 // already honour an ssh command, so a step needs no special handling: `git push` and
7 // `vole issue comment` authenticate as the account because the environment points at
8 // the key. The names are reserved in the workflow format, so a workflow cannot
9 // replace them.
10
11 import (
12 "fmt"
13 "net/url"
14 "strings"
15 )
16
17 // RunCredentialPath is where the key is written inside the job's container.
18 const RunCredentialPath = "/run/vole/id_bot"
19
20 // RunKnownHostsPath is where the instance's host key is written.
21 const RunKnownHostsPath = "/run/vole/known_hosts"
22
23 // withCredentialEnv returns a copy of the job's environment with the variables that
24 // point git and the CLI at the run credential. It copies rather than editing the
25 // pinned map, which belongs to the run record.
26 func withCredentialEnv(env map[string]string, remote string, job *runnerJob) (map[string]string, error) {
27 sshCommand, err := credentialSSHCommand(remote)
28 if err != nil {
29 return nil, err
30 }
31 out := make(map[string]string, len(env)+5)
32 for name, value := range env {
33 out[name] = value
34 }
35 out["VOLE_RUN_ID"] = job.ID
36 out["VOLE_REPO"] = job.Repo
37 out["VOLE_SSH_COMMAND"] = sshCommand
38 out["GIT_SSH_COMMAND"] = sshCommand
39 out["VOLE_KNOWN_HOSTS"] = RunKnownHostsPath
40 return out, nil
41 }
42
43 // credentialSSHCommand pins the key and the known_hosts file. IdentitiesOnly keeps
44 // ssh from offering an agent key that happens to be in the container, so the run
45 // credential is the only identity a step can present.
46 func credentialSSHCommand(remote string) (string, error) {
47 if _, _, err := splitRemote(remote); err != nil {
48 return "", err
49 }
50 return "ssh -i " + RunCredentialPath + " -o IdentitiesOnly=yes -o UserKnownHostsFile=" + RunKnownHostsPath, nil
51 }
52
53 // knownHostsLine is the entry that lets the job verify the instance it pushes to.
54 // OpenSSH brackets a host whose port is not the default.
55 func knownHostsLine(remote, hostKey string) (string, error) {
56 host, port, err := splitRemote(remote)
57 if err != nil {
58 return "", err
59 }
60 if strings.TrimSpace(hostKey) == "" {
61 return "", fmt.Errorf("the instance sent no host key with the job, so the credential has nothing to verify it against")
62 }
63 if port == "" || port == "22" {
64 return host + " " + strings.TrimSpace(hostKey) + "\n", nil
65 }
66 return "[" + host + "]:" + port + " " + strings.TrimSpace(hostKey) + "\n", nil
67 }
68
69 func splitRemote(remote string) (host, port string, err error) {
70 u, err := url.Parse(remote)
71 if err != nil || u.Scheme != "ssh" || u.Hostname() == "" {
72 return "", "", fmt.Errorf("%q is not an ssh URL, so the job cannot be told how to reach the instance", remote)
73 }
74 return u.Hostname(), u.Port(), nil
75 }