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/vole.git

Write over SSH with a registered key:

git clone ssh://git@rodent.software:2222/vole.git

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

Let a job declare the secrets it reads

4ac42680eca3181efc70099f65d6e2c9f45e9dd2 · Keenan Nemetz · 7d ago · parent 0c40a1d3

Let a job declare the secrets it reads

A job now names the repository secrets it needs, and the instance delivers
those values when the job is leased. The workflow carries names only, so the
queue row, the metadata branch and the run record never hold a value: the
record names the version of each value the job ran with, which is what makes a
result say which one it used.

    jobs:
      deploy:
        image: alpine:3.22
        secrets: [DEPLOY_KEY]

A name the repository has no value for is not a failure. The variable is left
unset rather than empty, the step's shell can tell the difference, and the
runner writes one line to the log saying which value was missing. That keeps
an optional secret optional and leaves "required" with the step, where someone
can write the message: ${DEPLOY_KEY:?DEPLOY_KEY is not set}.

The value reaches the step through a file the runner writes into the run
directory it already mounts at /run/vole for an event payload, and each step is
prefixed with `. /run/vole/secrets.env`. That is deliberately not an env-file
argument to the runtime, which cannot carry a line break, and not --env, which
puts the value in the process's argument list where every process on the host
can read it. A local `vole workflow run` writes the same file, reading the
values with the developer's own credential, and says which ones it could not
read.

The name rule and the 128 KiB limit moved to ci, next to the environment rules
they mirror, so the store refuses a name no workflow could declare and the
workflow refuses a name no process environment could carry.

Slice 2 of #154.

forge/secret/secret.go +10−14

@@ -26,7 +26,6 @@ import (
26 26 "fmt"
27 27 "io"
28 28 "os"
29 "regexp"
30 29 "strconv"
31 30 "strings"
32 31 "sync"
@@ -35,6 +34,7 @@ import (
35 34
36 35 _ "github.com/mattn/go-sqlite3"
37 36
37 "github.com/rodent-software/vole/ci"
38 38 "github.com/rodent-software/vole/internal/audit"
39 39 "github.com/rodent-software/vole/internal/secure"
40 40 )
@@ -46,11 +46,10 @@ const (
46 46 // MAX_ARG_STRLEN — 32 pages — which is the limit on a single string in a
47 47 // process's environment. A longer value could never reach a step, so refusing
48 48 // it here says why instead of failing deep inside a container.
49 MaxEntryBytes = 32 * 4096
49 MaxEntryBytes = ci.MaxSecretBytes
50 50 // formatVersion is the first byte of a sealed value. It names the cipher and
51 51 // the layout, so a later key or algorithm can be recognised without guessing.
52 52 formatVersion = 1
53 nameLimit = 64
54 53 )
55 54
56 55 // ErrNotFound means the repository has no secret with that name. Callers decide
@@ -58,8 +57,6 @@ const (
58 57 // variable.
59 58 var ErrNotFound = errors.New("secret: no such secret")
60 59
61 var nameRE = regexp.MustCompile(`^[A-Z][A-Z0-9_]{0,63}$`)
62
63 60 // Info is one secret's metadata. It never holds a value: the settings page and
64 61 // `vole secret list` show what a secret is, not what it says.
65 62 type Info struct {
@@ -127,16 +124,15 @@ CREATE TABLE IF NOT EXISTS secrets (
127 124
128 125 func (s *Store) Close() error { return s.db.Close() }
129 126
130 // ValidateName reports why a name cannot be a secret. A name is an environment
131 // variable's name, so a step reads it as $NAME with nothing to translate.
127 // ValidateName reports why a name cannot be a secret. The rule lives in the ci
128 // package because it is the workflow language's rule too: the store refuses a name
129 // no workflow could declare, so a value that cannot be used is not stored.
132 130 func ValidateName(name string) error {
133 switch {
134 case name == "":
135 return errors.New("a secret needs a name")
136 case len(name) > nameLimit:
137 return fmt.Errorf("%q is %d bytes; a secret name is at most %d", name, len(name), nameLimit)
138 case !nameRE.MatchString(name):
139 return fmt.Errorf("%q is not a secret name (upper-case letters, digits and _, starting with a letter)", name)
131 if err := ci.ValidateSecretName(name); err != nil {
132 return err
133 }
134 if ci.ReservedEnvName(name) {
135 return fmt.Errorf("%s is set by the runner, so it cannot be a secret", name)
140 136 }
141 137 return nil
142 138 }