Range-diff !498

back to !498 backup: encrypt archives to age recipients

 -:  ------- >  1:  9002a0b control: move the feed-line sentence renderer from httpd, so the CLI can share it
 -:  ------- >  2:  e0ae401 feed: a labelled event names the labels
 -:  ------- >  3:  cf66fc3 dashboard, feed: render activity as the web's sentence, not the raw payload
 -:  ------- >  4:  6dae104 feedline: extract FeedLine.Sentence, dedupe runDashboard/runFeed
 -:  ------- >  5:  2737992 dashboard: an assigned issue no longer repeats under open issues
 -:  ------- >  6:  b39ffae notifications list: name --all when the empty inbox is just read items
 -:  ------- >  7:  061bd06 feedline: space between the repository and a tag, job or sha
 -:  ------- >  8:  3815908 notifications list: name --all only when read items exist
 -:  ------- >  9:  45230cb e2e: an assigned issue is not listed under open_issues
 -:  ------- > 10:  d2db7d8 changelog: unreleased entry for #265
 -:  ------- > 11:  a3fa0f0 usage, help: print the program and the CLI's own path for the command
 -:  ------- > 12:  a8523b1 cli: send --path= where the CLI path differs from the server path
 -:  ------- > 13:  a1e67e4 e2e: usage prints the CLI's own path, stock ssh the registered one
 -:  ------- > 14:  d905c69 flags: print a bad-flag usage line the way a usage refusal does
 -:  ------- > 15:  44191b1 auth keys add, pgp add: check --help before reading stdin
 -:  ------- > 16:  4eb0f95 help: auth (and any future CLI-only grouping) renders with the registry layout, in the CLI's own paths
 -:  ------- > 17:  7664da8 auth --help: force --path= for a bare CLI-only alias group
 -:  ------- > 18:  d5968a8 summaries: verb phrases instead of bare nouns
 -:  ------- > 19:  004ff6f control, cmd/gitbay: guard nounAliases and aliasGroupNames against drift
 -:  ------- > 20:  ba0a7d3 changelog: #267 and the --path= upgrade note
 -:  ------- > 21:  fd270da help: no auth <verb> usage or footer over stock ssh
 -:  ------- > 22:  bc76b6f changelog: usage refusals name ssh git@<host> outside the CLI
 -:  ------- > 23:  3595439 sshd: unregistered-key message names the fingerprint and the real host
 -:  ------- > 24:  e3ba425 issue create: --label, --milestone, --assignee
 -:  ------- > 25:  ea552d4 mr show: pluralize commits/checks/reviews section headings
 -:  ------- > 26:  86c82f6 repo readme: print a repository's README, the web page's file order
 -:  ------- > 27:  6bf4591 repo show: truncate the mirror's last-sync time to the second
 -:  ------- > 28:  4fa1930 issue create: resolve milestone and assignees first, set fields through issue label/milestone/assign
 -:  ------- > 29:  79fdea2 sshd: unregistered-key settings link from the site URL, scheme and port kept
 -:  ------- > 30:  a4152dd wiki: issue create synopsis names --label, --milestone, --assignee
 -:  ------- > 31:  0338e6a store: run foreign_key_check inside the migration transaction, before commit
 -:  ------- > 32:  d0e26d3 web: pin and watch toggles dispatch through repo pin/watch/mute/unwatch
 -:  ------- > 33:  8a379d4 web: Cache-Control: no-store on the login-link request
 -:  ------- > 34:  4ffdc2c wiki: fix API token-refusal claim, batched-review status, watch/pin dispatch, login-link URL exception
 -:  ------- > 35:  9bcf573 account: quote whoami, token create and auth export in forms that run
 -:  ------- > 36:  0e46097 cmd/gitbay: test every quoted web command against the registry
 -:  ------- > 37:  f4f897e web: range-diff page for a merge request's revisions
 -:  ------- > 38:  e0a00fb web: list each MR revision with a compare-to-previous link
 -:  ------- > 39:  6ddd002 wiki: Parity reflects the web range-diff view
 1:  848f79b = 40:  ca8187d runner next: say whether the build is trusted
 2:  5f01597 = 41:  94f55ff runner: disposable home for untrusted builds
 3:  bb4a146 ! 42:  7b64442 wiki: trusted and untrusted build homes
    @@ .gitbay/wiki/Admin.org: allocates without bound, and it sits above the e2e suite
      ## .gitbay/wiki/Architecture/04-Trust-Boundaries.org ##
     @@
      | TB4 | Z1 → Z3 git                        | argv, repository path, stdin packs               | argv built by code, never a shell; repository path from the database, not the request (=internal/gitutil=) |
    - | TB5 | Z3 → Z1 hook socket                | ref updates, repository id, user id, key scope, commit objects | the daemon decides with =policy.CheckPush= and =sig.VerifyCommit= (=internal/hookd/hookd.go=). The socket trusts the ids in the request, so access to the socket is equivalent to acting as any user; it is reachable only through the =gitbay= user's filesystem |
    + | TB5 | Z3 → Z1 hook socket                | ref updates, repository id, user id, key scope, push token, commit objects | the socket is mode 0600 and, on Linux, refuses a peer whose uid is not the daemon's; a request must carry the token sshd minted for its receive-pack (stored hashed in =push_tokens=) and name the same repository, account and scope. The daemon then decides with =policy.CheckPush= and =sig.VerifyCommit= (=internal/hookd/hookd.go=) |
      | TB6 | Z4 ↔ Z1 runner channel             | build claims (with secrets for trusted builds), logs, results | runner-scoped SSH key; claims limited to attached repositories; secrets only when the build is trusted (=internal/control/build.go=) |
     -| TB7 | Z5 → Z4 container                  | build steps, workspace, build home               | rootless podman, operator-provisioned image, cgroup limits; the build home is shared per repository and the network is open (#255, #260) |
     +| TB7 | Z5 → Z4 container                  | build steps, workspace, build home               | rootless podman, operator-provisioned image, cgroup limits; a trusted build's home is its repository's, an untrusted build's is discarded with it; the network is open (#260) |
    @@ .gitbay/wiki/Architecture/10-Known-Gaps.org: what the 2026-09-27 review found; r
      | Issue | Area             | Gap                                                                   | Severity |
      |-------+------------------+-----------------------------------------------------------------------+----------|
     -| #255  | CI isolation     | Untrusted and trusted builds of a repository share a writable build home | high  |
    - | #256  | Authentication   | A removed SSH key keeps working on connections already open           | high     |
    - | #257  | Credentials      | An expiring token can create credentials that outlive it; tokens default to full scope | high |
      | #258  | CI integrity     | Any writer can post a =ci/*= status; tree reuse ignores trust and image | high   |
    + | #259  | Recovery         | No restore has been exercised; verification does not check git connectivity | high |
    + | #260  | CI network       | Builds share the runner's source address; no egress policy            | medium   |
     
      ## .gitbay/wiki/Threat-Model.org ##
     @@ .gitbay/wiki/Threat-Model.org: runner, polling over SSH, clones the commit and runs its steps.
 4:  2e830df = 43:  b022fed status set: ci/ is reserved for the instance's builds
 5:  3453d63 = 44:  b1f4bf1 ci: reuse only trusted results on the job's image
 6:  bba63a3 ! 45:  dc10160 repo settings: required contexts turn the checks gate on, pending until reported
    @@ internal/control/mr.go: func MergeGates(st *store.Store, repo store.Repo, mr sto
      	}
     
      ## internal/control/mr_test.go ##
    -@@ internal/control/mr_test.go: package control
    - import (
    - 	"bytes"
    +@@ internal/control/mr_test.go: import (
      	"encoding/json"
    + 	"os"
    + 	"path/filepath"
     +	"slices"
      	"strconv"
      	"strings"
      	"testing"
    -@@ internal/control/mr_test.go: func TestMREditSupersededByOnOpenMRRefused(t *testing.T) {
    - 		t.Fatalf("stderr = %q, want the closed-only refusal", errOut.String())
    +@@ internal/control/mr_test.go: func TestMRShowPluralizesMultiRowSections(t *testing.T) {
    + 		}
      	}
      }
     +
 7:  60cf9c5 ! 46:  d19e518 wiki: reserved ci/ statuses, trusted reuse, required contexts
    @@ .gitbay/wiki/Architecture/09-Controls.org: chapter names of OWASP ASVS 4.0 where
     
      ## .gitbay/wiki/Architecture/10-Known-Gaps.org ##
     @@ .gitbay/wiki/Architecture/10-Known-Gaps.org: what the 2026-09-27 review found; remove a row when its issue closes.
    + 
    + | Issue | Area             | Gap                                                                   | Severity |
      |-------+------------------+-----------------------------------------------------------------------+----------|
    - | #256  | Authentication   | A removed SSH key keeps working on connections already open           | high     |
    - | #257  | Credentials      | An expiring token can create credentials that outlive it; tokens default to full scope | high |
     -| #258  | CI integrity     | Any writer can post a =ci/*= status; tree reuse ignores trust and image | high   |
      | #259  | Recovery         | No restore has been exercised; verification does not check git connectivity | high |
      | #260  | CI network       | Builds share the runner's source address; no egress policy            | medium   |
 8:  734d0a4 <  -:  ------- changelog: #255, #258
 -:  ------- > 47:  7a5f2a1 changelog: #255, #258
 9:  ef74a96 ! 48:  42a7b12 wiki, changelog: last finisher sets ci/<job>; required contexts report on heads
    @@ .gitbay/wiki/CI.org: Rows worth a second look:
      and the store, and =TestPushShapesTableOnWiki= checks that this page
     
      ## CHANGELOG.org ##
    -@@ CHANGELOG.org: anything beyond "replace the binary and restart" is needed.
    - 
    - * Unreleased
    - 
    --- Untrusted builds (merge requests from forks) get a fresh HOME removed after the build and no secrets; trusted builds keep a per-repository home under =<workdir>/trusted-home=. Deploy gitbayd before the runner; the old shared homes under the runner's workdir can be deleted. (#255)
    --- =status set= refuses =ci/= contexts, which belong to the instance's builds. Build results are reused only from trusted builds on the same image. =repo settings require-contexts= names status contexts that must report green; setting any turns require-checks on, and one not yet reported counts as pending. (#258)
    +@@ CHANGELOG.org: for the eighteen commands whose CLI path differs from the registry's
    +   browser (#269).
    + - Untrusted builds (merge requests from forks) get a fresh HOME removed after the build and no secrets; trusted builds keep a per-repository home under =<workdir>/trusted-home=. Deploy gitbayd before the runner; the old shared homes under the runner's workdir can be deleted. (#255)
    + - =status set= refuses =ci/= contexts, which belong to the instance's builds. Build results are reused only from trusted builds on the same image. =repo settings require-contexts= names status contexts that must report green; setting any turns require-checks on, and one not yet reported counts as pending. (#258)
     +- Untrusted builds (merge requests from forks) get a fresh HOME
     +  removed after the build and no secrets; trusted builds keep a
     +  per-repository home under =<workdir>/trusted-home=. Deploy gitbayd
10:  ee343ff = 49:  c21c7ea runner next: send the instance's public ssh destination
11:  457dbd2 = 50:  28f6758 runner: builds off the host's loopback when the runner polls over it
12:  debffbc ! 51:  23e979a sshd: test the auth limiter's lockout by registration mode
    @@ Commit message
         Ref #260
     
      ## internal/sshd/sshd_test.go ##
    -@@ internal/sshd/sshd_test.go: func TestStopEndsFollow(t *testing.T) {
    - 		t.Errorf("stderr %q", stderr.String())
    +@@ internal/sshd/sshd_test.go: func TestUnregisteredKeyMessageNamesFingerprintAndHost(t *testing.T) {
    + 		}
      	}
      }
     +
    @@ internal/sshd/sshd_test.go: func TestStopEndsFollow(t *testing.T) {
     +	return &Server{cfg: cfg, st: st, authLimiter: newRateLimiter(3, time.Minute)}, runner
     +}
     +
    -+var (
    -+	fromLoopback = authMeta{addr: &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 40000}}
    -+	fromPublic   = authMeta{addr: &net.TCPAddr{IP: net.IPv4(203, 0, 113, 7), Port: 40000}}
    -+)
    -+
     +// With registration closed an unknown key counts against its address.
     +// Below the limit a known key's success clears the count. At the limit
     +// authenticate refuses before it looks at the key, so the runner's own
    @@ internal/sshd/sshd_test.go: func TestStopEndsFollow(t *testing.T) {
     +		}
     +	}
     +}
    ++
    ++var (
    ++	fromLoopback = authMeta{addr: &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 40000}}
    ++	fromPublic   = authMeta{addr: &net.TCPAddr{IP: net.IPv4(203, 0, 113, 7), Port: 40000}}
    ++)
13:  6caa5ef = 52:  9809a86 runner host: builds reach only the forge's public ports on it
14:  32cf32b = 53:  9467ed2 runner egress: remove the rule when it blocks the runner's poll
15:  d50a925 ! 54:  26e387e wiki: what a build can reach
    @@ .gitbay/wiki/Admin.org: The script installs podman, delegates a subuid/subgid ra
      ## .gitbay/wiki/Architecture/04-Trust-Boundaries.org ##
     @@
      | TB4 | Z1 → Z3 git                        | argv, repository path, stdin packs               | argv built by code, never a shell; repository path from the database, not the request (=internal/gitutil=) |
    - | TB5 | Z3 → Z1 hook socket                | ref updates, repository id, user id, key scope, commit objects | the daemon decides with =policy.CheckPush= and =sig.VerifyCommit= (=internal/hookd/hookd.go=). The socket trusts the ids in the request, so access to the socket is equivalent to acting as any user; it is reachable only through the =gitbay= user's filesystem |
    + | TB5 | Z3 → Z1 hook socket                | ref updates, repository id, user id, key scope, push token, commit objects | the socket is mode 0600 and, on Linux, refuses a peer whose uid is not the daemon's; a request must carry the token sshd minted for its receive-pack (stored hashed in =push_tokens=) and name the same repository, account and scope. The daemon then decides with =policy.CheckPush= and =sig.VerifyCommit= (=internal/hookd/hookd.go=) |
      | TB6 | Z4 ↔ Z1 runner channel             | build claims (with secrets for trusted builds), logs, results | runner-scoped SSH key; claims limited to attached repositories; secrets only when the build is trusted (=internal/control/build.go=) |
     -| TB7 | Z5 → Z4 container                  | build steps, workspace, build home               | rootless podman, operator-provisioned image, cgroup limits; a trusted build's home is its repository's, an untrusted build's is discarded with it; the network is open (#260) |
     +| TB7 | Z5 → Z4 container                  | build steps, workspace, build home               | rootless podman, operator-provisioned image, cgroup limits; a trusted build's home is its repository's, an untrusted build's is discarded with it; outbound is open; on the host only the forge's public ports (#260) |
    @@ .gitbay/wiki/Users.org: with =sh -c= on the instance's runner, stopping at the f
      the build's container has no key of its own. Two things about that
     
      ## CHANGELOG.org ##
    -@@ CHANGELOG.org: anything beyond "replace the binary and restart" is needed.
    +@@ CHANGELOG.org: for the eighteen commands whose CLI path differs from the registry's
        same image. =repo settings require-contexts= names status contexts
        that must report green; setting any turns require-checks on, and one
        not yet reported counts as pending. (#258)
16:  b32d8de ! 55:  24c714d runner: builds reach the forge at pasta's host address
    @@ .gitbay/wiki/Users.org: with =sh -c= on the instance's runner, stopping at the f
      the build's container has no key of its own. Two things about that
     
      ## CHANGELOG.org ##
    -@@ CHANGELOG.org: anything beyond "replace the binary and restart" is needed.
    +@@ CHANGELOG.org: for the eighteen commands whose CLI path differs from the registry's
        same image. =repo settings require-contexts= names status contexts
        that must report green; setting any turns require-checks on, and one
        not yet reported counts as pending. (#258)
17:  2e26fae = 56:  dcd9380 runner: only a loopback runner's podman builds leave -remote
18:  c3ec5a0 ! 57:  b4e14d4 store: failed step and reason on a build
    @@ internal/store/builds_test.go: func TestBuildLogFrom(t *testing.T) {
     +	}
     +}
     
    - ## internal/store/migrations/0060_build_failure.down.sql (new) ##
    + ## internal/store/migrations/0065_build_failure.down.sql (new) ##
     @@
     +ALTER TABLE builds DROP COLUMN failed_reason;
     +ALTER TABLE builds DROP COLUMN failed_step;
     
    - ## internal/store/migrations/0060_build_failure.up.sql (new) ##
    + ## internal/store/migrations/0065_build_failure.up.sql (new) ##
     @@
     +-- Where a failed build stopped: the 1-based step, 0 when it stopped
     +-- before any step or did not fail, and the runner's one-line reason.
19:  c413cd8 = 58:  6421e2f runner done: record the failed step and reason
20:  b3597da = 59:  286e0c3 runner: name the failed step and report it
21:  ba1f3e1 ! 60:  e6b51c1 build show: failed step and duration; build log --step, --tail
    @@ Commit message
         Ref #266
     
      ## CHANGELOG.org ##
    -@@ CHANGELOG.org: anything beyond "replace the binary and restart" is needed.
    +@@ CHANGELOG.org: for the eighteen commands whose CLI path differs from the registry's
        =deploy/runner-podman-setup.sh= (it installs nftables) before =make
        deploy-runner=. Deploy gitbayd, then the runner, after validating on
        a scratch repository per the CI page. (#260)
    @@ internal/control/build.go: func runBuildShow(c *Ctx, args []string) int {
      }
      
      func runBuildLog(c *Ctx, args []string) int {
    --	f, err := parseFlags(args, flagSpec{Bools: []string{"--follow"}, MaxPos: 2, Usage: c.Cmd.Usage})
    -+	f, err := parseFlags(args, flagSpec{Bools: []string{"--follow"}, Values: []string{"--step", "--tail"}, MaxPos: 2, Usage: c.Cmd.Usage})
    +-	f, err := c.parseArgs(args, flagSpec{Bools: []string{"--follow"}, MaxPos: 2, Usage: c.Cmd.Usage})
    ++	f, err := c.parseArgs(args, flagSpec{Bools: []string{"--follow"}, Values: []string{"--step", "--tail"}, MaxPos: 2, Usage: c.Cmd.Usage})
      	if err != nil {
      		return c.fail(protocol.ExitUsage, "%v", err)
      	}
22:  b11b571 = 61:  ec5fe62 web: build log folded by step, failed step open
23:  396c1c3 = 62:  bb4c0ae wiki: failed step, build log --step and --tail
24:  9ddb593 ! 63:  3bcdce3 web: step fold shows the step's first line; changelog: deploy order
    @@ Commit message
         Ref #266
     
      ## CHANGELOG.org ##
    -@@ CHANGELOG.org: anything beyond "replace the binary and restart" is needed.
    +@@ CHANGELOG.org: for the eighteen commands whose CLI path differs from the registry's
        a scratch repository per the CI page. (#260)
      - =build show= names a failed build's step and duration; =build log
        --step <n>|failed --tail <lines>= reads one step's output or the
 -:  ------- > 64:  e5b80f7 control: runner done parses its flags through c.parseArgs
25:  b777bd1 = 65:  ed99c93 seal: AES-256-GCM keyring for secret columns
26:  5cfa94a ! 66:  b13a247 config: server.secret_key_file, outside server.root
    @@ internal/config/config.go: func (c Config) Validate() error {
      	}
     
      ## internal/config/config_test.go ##
    -@@ internal/config/config_test.go: func TestPushHost(t *testing.T) {
    - 		t.Fatalf("GITBAY_APNS_HOST ignored: %q", got)
    +@@ internal/config/config_test.go: func TestMailTLSRequired(t *testing.T) {
    + 		}
      	}
      }
     +
27:  b418578 ! 67:  1ec2c9c store: seal CI secrets, webhook secrets, mirror tokens and device tokens
    @@ internal/store/cisecrets.go: func (s *Store) BuildSecrets(repoID int64) (map[str
      	return out, rows.Err()
      }
     
    - ## internal/store/migrations/0061_push_token_hash.down.sql (new) ##
    + ## internal/store/migrations/0066_push_token_hash.down.sql (new) ##
     @@
     +DROP INDEX push_devices_token_hash;
     +ALTER TABLE push_devices DROP COLUMN token_hash;
     
    - ## internal/store/migrations/0061_push_token_hash.up.sql (new) ##
    + ## internal/store/migrations/0066_push_token_hash.up.sql (new) ##
     @@
     +-- APNs tokens are sealed with a random nonce (internal/seal), so two
     +-- stores of one token differ; lookups and the re-registration upsert go
    @@ internal/store/store.go: import (
      )
      
     @@ internal/store/store.go: type Store struct {
    - 	// by the next change to that build's row (BuildLogWait).
    - 	logMu   sync.Mutex
    - 	logWait map[int64]chan struct{}
    -+
    + 	// daemon sets it to its own logger, whose output the service
    + 	// journal keeps outside the database.
    + 	AuditJournal *slog.Logger
     +	// secrets seals and opens the secret columns (secrets.go). Nil
     +	// stores values as given and refuses to open sealed ones.
     +	secrets *seal.Keyring
28:  9533944 ! 68:  24b9cdb gitbayd: load the secret key file; admin secrets init, rotate, check
    @@ cmd/gitbayd/main.go: import (
      	// which makes an unexpected schema version hard to attribute to the deploy
      	// that caused it.
     @@ cmd/gitbayd/main.go: func serveCmd() *cobra.Command {
    - 				return err
    - 			}
    - 			defer st.Close()
    + 			// audit row outside the database the daemon can write. Rows
    + 			// are logged at Info, which the default handler always emits.
    + 			st.AuditJournal = slog.Default()
     +			// Values stored before sealing existed, or under a key a
     +			// rotation retired, are sealed under the current key before
     +			// anything reads them. A value the key file cannot open
    @@ cmd/gitbayd/main.go: func serveCmd() *cobra.Command {
      			// start the hook policy socket.
     @@ cmd/gitbayd/main.go: func adminCmd() *cobra.Command {
      		hostCmd("runners [--json]", "runner accounts: last poll, scope, the build each holds", "admin", "runners"),
    - 		hostCmd("audit [--limit n] [--json]", "print the security audit log, newest first", "audit"),
    + 		auditCmd,
      		backupCmd(),
     +		secretsCmd(),
      		gcCmd(),
29:  9c0057a = 69:  1481a12 e2e: a key file per instance; the archive carries secrets sealed and no key
30:  eb79a68 ! 70:  7e5de53 deploy, wiki: provision and document the secret key file
    @@ .gitbay/wiki/Architecture/06-Data-and-Cryptography.org
      * Data inventory
      
     -Schema: =internal/store/migrations/=, 59 migrations. Classification:
    -+Schema: =internal/store/migrations/=, 61 migrations. Classification:
    ++Schema: =internal/store/migrations/=, 66 migrations. Classification:
      *C* credential or secret, *P* personal data, *R* private repository
      content (as confidential as the repository), *O* operational.
      
    @@ .gitbay/wiki/Architecture/06-Data-and-Cryptography.org: content (as confidential
     +| Integrations    | =webhooks= (secret), =webhook_deliveries=, =mirrors= (username, token)                      | C     | webhook secret and mirror token sealed          |
     +| Notifications   | =notifications= (mail queue), =inbox=, =push_devices= (APNs token), =push_queue=            | P     | device tokens sealed; looked up by SHA-256      |
      | Signatures      | =commit_signatures=, =settings.key_epoch=                                                   | O     | verification cache                              |
    - | Audit and feed  | =audit_log=, =events=                                                                       | O, P  | actor ids, pruned argv, fingerprints and IPs in some audit rows |
    + | Audit and feed  | =audit_log=, =events=                                                                       | O, P  | actor ids, pruned argv, fingerprints and IPs in some audit rows, a hash chain (=prev_hash=, =hash=) |
      | Dependencies    | =dep_checks=, =dep_reports=                                                                 | O     |                                                 |
     @@ .gitbay/wiki/Architecture/06-Data-and-Cryptography.org: Outside the database:
      | TLS keys (ACME)            | =<root>/acme=                     | C     |
    @@ .gitbay/wiki/Architecture/10-Known-Gaps.org: what the 2026-09-27 review found; r
      | #262  | Availability     | No limit on concurrent git pack generation                            | high     |
     -| #273  | Data at rest     | CI secrets, webhook secrets and mirror tokens are stored in clear in SQLite | high |
      | #274  | Backups          | The local backup archive is not encrypted                             | medium   |
    - | #275  | Audit            | Refused writes are not audited; the audit table is writable by the daemon user | medium |
    - | #276  | Sessions         | Web sessions last 7 days with no idle timeout                         | low      |
    + | #298        | SSRF       | =repo import --from= fetches without an address check                 | medium   |
    + | #297  | Credentials      | A browser session can mint tokens and keys that outlive it            | low      |
     
      ## CHANGELOG.org ##
    -@@ CHANGELOG.org: anything beyond "replace the binary and restart" is needed.
    - 
    - * Unreleased
    - 
    +@@ CHANGELOG.org: for the eighteen commands whose CLI path differs from the registry's
    +   browser (#269).
    + - Untrusted builds (merge requests from forks) get a fresh HOME removed after the build and no secrets; trusted builds keep a per-repository home under =<workdir>/trusted-home=. Deploy gitbayd before the runner; the old shared homes under the runner's workdir can be deleted. (#255)
    + - =status set= refuses =ci/= contexts, which belong to the instance's builds. Build results are reused only from trusted builds on the same image. =repo settings require-contexts= names status contexts that must report green; setting any turns require-checks on, and one not yet reported counts as pending. (#258)
     +*Upgrade note.* gitbayd needs =server.secret_key_file= (default
     +=/etc/gitbay/secret.key=) and refuses to start without it. Before
     +replacing the binary, run =gitbayd admin secrets init= as root and
    @@ CHANGELOG.org: anything beyond "replace the binary and restart" is needed.
     +earlier release after values are sealed is not supported: an older
     +gitbayd reads a sealed value's =gbs1:...= prefix as the literal
     +secret.
    -+
     +- CI secrets, webhook secrets, mirror tokens and push device tokens are
     +  stored sealed with AES-256-GCM (#273). =gitbayd admin secrets
     +  init|rotate|check=.
31:  510db8a = 71:  03e5ee3 config: [backup] age_recipients (filippo.io/age v1.3.2)
32:  19aff4d = 72:  12a6859 backup: encrypt archives to [backup] age_recipients; --verify --identity
33:  fa1c80f ! 73:  03757af deploy, wiki: encrypted archives in the backup scripts and docs
    @@ .gitbay/wiki/Architecture/10-Known-Gaps.org: what the 2026-09-27 review found; r
      | #261  | Various          | Migration foreign-key check after commit; three web writes bypass dispatch; documentation drift | medium |
      | #262  | Availability     | No limit on concurrent git pack generation                            | high     |
     -| #274  | Backups          | The local backup archive is not encrypted                             | medium   |
    - | #275  | Audit            | Refused writes are not audited; the audit table is writable by the daemon user | medium |
    - | #276  | Sessions         | Web sessions last 7 days with no idle timeout                         | low      |
    - | #277  | Credentials      | SSH and deploy keys never expire                                      | low      |
    +-| #298        | SSRF       | =repo import --from= fetches without an address check                 | medium   |
    ++| #298  | SSRF             | =repo import --from= fetches without an address check                 | medium   |
    + | #297  | Credentials      | A browser session can mint tokens and keys that outlive it            | low      |
    + 
    + * Not filed
     
      ## CHANGELOG.org ##
     @@ CHANGELOG.org: secret.