Wiki: Admin
gitbay admin guide
- Install
- Configuration reference
- Users, email, invites
- Audit and account control
- Maintenance
- Backup and restore
- Upgrades
- CI runner
- LFS storage
- Pages
- Security
- Odds and ends
One static binary (gitbayd), one SQLite file, bare repositories on
disk, and the system git. Schema migrations run automatically on
startup and on every admin command.
Install
Build from source (go build ./cmd/gitbayd), install via the vanity
module path (go install gitbay.org/gitbay/cmd/gitbayd@latest), or use
a release build: deploy/release.sh <tag> cross-compiles reproducible
linux/amd64, linux/arm64, and darwin/arm64 binaries with a SHA256SUMS
manifest (CGO off, trimpath, stripped — byte-identical per commit and
toolchain).
install -m 755 gitbayd /usr/local/bin/
adduser --system --group --home /var/lib/gitbay --shell /usr/sbin/nologin gitbay
install -d -o gitbay -g gitbay -m 750 /var/lib/gitbay
gitbayd --config /etc/gitbay/config.toml check-config
deploy/ in the source tree has a cloud-init file, a hardened systemd
unit, and a nightly backup timer. Run as the unprivileged gitbay user;
the unit's AmbientCapabilities=CAP_NET_BIND_SERVICE covers ports
22/80/443 without root.
The SSH port decision
ssh.mode = "embedded"(default): gitbayd itself listens, normally on 22 — move the host's admin sshd to another port. Remotes readgit@host:owner/repowith no port gymnastics.-
ssh.mode = "system": the host sshd owns 22 and invokes gitbayd viaAuthorizedKeysCommand:AuthorizedKeysCommand /usr/local/bin/gitbayd --config /etc/gitbay/config.toml authorized-keys %t %k AuthorizedKeysCommandUser gitbay
sshd requires that binary to be root-owned and not group/world writable. Unknown keys fail authentication inside sshd, so system mode requires
registration.mode = "closed"(check-config enforces this).
Configuration reference
/etc/gitbay/config.toml. check-config validates and names every
contradiction; --no-host-checks skips port/path probes.
[server]
root(default/var/lib/gitbay) — repositories, database, host keys, ACME cache all live here.site_url(required) — canonicalhttps://host; drives ACME, clone URLs, mail links.source_repo(optional,owner/name) — the repository this instance develops itself in. Startup warns when the running build's commit is not on that repository's default branch, which is how a binary built from an unmerged branch stops being invisible. Leave it unset unless the instance hosts its own source.
[ssh]
mode—embedded|system(above).port(22) — embedded listener port.host_keys— list of private key paths; empty generates an ed25519 key at<root>/ssh/host_ed25519.
[http]
addr(:443),tls—acme|files|off.acme: certificates via TLS-ALPN-01 on the HTTPS port, cached at<root>/acme;acme_emailfor the CA account;acme_http_addr(:80,"off"to disable) adds HTTP-01 and an https redirect — failing to bind it is a warning, not fatal. Requires anhttps://site_url with a public DNS name.files:cert_file+key_file.off: plain HTTP — development, or behind a TLS-terminating proxy.
[web]
mode—view_only(default) |accounts. In view_only the mutating web routes are never registered; in accounts, browser sessions are minted over SSH (web login), and users with write access can create repos, comment, and make simple file edits (which commit unsigned, honestly).password_authis reserved and currently rejected.
[registration]
mode—closed(default) |invite|open. invite/open require [mail]. See the user guide for the flows.
[mail]
smtp_host(host:port, 587 assumed),from, optionalsmtp_user/smtp_pass. STARTTLS when offered. Required for invite/open registration and self-serviceemail add; in closed mode you may omit it entirely and assert addresses by hand (below).
[api]
enabled(false) — the JSON API surface; see API. Off means no credential-bearing HTTP endpoint exists at all.
[webhooks]
allow_local(false) — permit webhook targets on loopback/private addresses. Leave off unless you know why you need it (SSRF).
[limits]
clone_timeout(3600s) — cap onrepo importfetches.max_blob_bytes(100MB) — cap on raw file serving over the web.max_asset_bytes(512MB) — cap per uploaded release asset.max_pack_bytes,ssh_auth_rate— reserved, not yet enforced.
[git_daemon]
enabled(false),port(9418) — the anonymousgit://listener. Serves only public repositories that additionally ranrepo settings git-daemon <repo> on.
[mirrors]
pull_interval_minutes(15) — how often pull mirrors fetch their upstream. Push mirrors sync shortly after each local ref update. Mirror URLs pass the same SSRF rules as webhook targets.
[go_import]
Vanity Go module paths, one per line: "host/module" = "owner/repo".
Requests with ?go-get=1 at or under the module path answer with the
go-import meta tag pointing at the repository's HTTPS clone URL, so
go install host/module/cmd/...@latest resolves. The repository should
be public (the module path itself confirms it exists).
Users, email, invites
gitbayd admin user create alice --key alice.pub --email a@example.org --verified [--admin]
gitbayd admin email verify alice a@example.org # admin assertion, no SMTP needed
gitbayd admin invite --email b@example.org # mails a code; prints it if no SMTP
"Verified" means SMTP-confirmed or host-admin-asserted; the database
records which. Verified emails are what make commit signatures
meaningful — an unverified address never produces a verified badge.
Audit and account control
The audit log is the security feed (events are the product feed): every successful mutating command with its argv and source credential (SSH key fingerprint or API), registrations, admin actions, force-pushes, and auth failures/throttling. Secrets never appear — they travel on stdin, never in argv.
gitbayd admin audit [--limit n] # host-local
ssh git@<host> audit [--limit n] # instance admins, SSH only
gitbayd admin user disable <name> # suspend: SSH, web, API all refused;
gitbayd admin user enable <name> # sessions dropped, nothing deleted
gitbayd admin user delete <name> --yes # only for accounts anchoring nothing:
# refused (with each blocker named) while
# the account owns repos, authored
# issues/MRs/comments/reviews, or is an
# org's only admin
limits.ssh_auth_rate (10) throttles per-IP authentication failures
per minute — successful auths never count and clear the slate.
limits.max_pack_bytes is enforced as receive.maxInputSize on every
push.
Maintenance
gitbayd admin stats [--json] # counts, database size, per-repo disk
gitbayd admin gc [--repo owner/name] # git gc: repack and prune; per-repo sizes
gitbayd admin gc --aggressive # thorough repack; slow, rarely needed
deploy/cloud-init.yaml ships a gitbay-gc.timer that runs admin gc
weekly (Sunday 07:00 UTC). Imported repositories keep whatever pack
layout the source sent, so a first manual admin gc after a bulk
import is worthwhile.
Backup and restore
gitbayd admin backup --out /var/backups/gitbay/backup.tar.gz
One archive: a consistent SQLite snapshot (taken before the repositories are read, so the database never references objects the archive missed), every repository, and the SSH host keys. Excluded: hook socket, regenerated hook scripts, WAL files. Safe to run against a live daemon.
Restore: extract into an empty directory, point server.root at it,
start gitbayd. Host keys are preserved, so clients keep their
known_hosts entries; hooks regenerate at startup.
Upgrades
Replace the binary, restart the unit. Migrations apply automatically and
are transactional; hook scripts under <root>/hooks are rewritten at
startup to point at the current binary path.
CI runner
gitbay-runner executes builds queued by pushes. It polls over SSH as
an admin account (runner commands are admin-only: a runner executes
arbitrary repo code), clones, runs the steps, streams the log back, and
resolves the commit status. Run it as a dedicated unprivileged user:
useradd --system --create-home --home-dir /var/lib/gitbay-runner ci-runner
sudo -u ci-runner ssh-keygen -t ed25519 -N "" -f /var/lib/gitbay-runner/.ssh/id_ed25519
gitbayd --config /etc/gitbay/config.toml admin user create ci --admin \
--key /var/lib/gitbay-runner/.ssh/id_ed25519.pub
gitbay-runner -remote git@127.0.0.1 -workdir /var/lib/gitbay-runner/work
v1 runs steps directly on the host — no containers — so treat the runner machine as executing whatever your users push. Install the toolchains your builds need on it.
Instance admin on the runner account only authorizes the claim/report
protocol; it grants no repo access. A build that pushes back — a pages
deploy, an archive publish, an automated MR branch — needs an explicit
grant on that repo: repo access grant <owner/name> ci write. Private
repos likewise need at least read for the clone.
LFS storage
Objects live content-addressed under [lfs] root (default
<server.root>/lfs); [lfs] max_object_bytes caps a single object
(512MB default). Storage sits behind a small interface — an
S3-compatible backend is a drop-in with the server proxying, and
presigned URLs a later optimization. LFS objects do not travel with
push mirrors (mirrors move git refs only), and gc does not yet collect
orphaned objects.
Pages
[pages] domain = "example.site" serves public repos' pages branches
on <owner>.<domain>. DNS needs a wildcard record *.<domain> to the
server; ACME issues per-subdomain certificates on demand (only for
owners that exist). The domain must not be the site host or a parent of
it — pages content runs its own scripts and must stay off the forge's
origin.
Users with repo admin claim custom domains with repo domain add.
Claims activate only after a DNS TXT challenge proves control of the
domain (repo domain verify, audit-logged); pending claims serve
nothing, get no certificates, and expire after 7 days. ACME issues
certificates only for verified hosts, so stray DNS pointed at the
server gets nothing.
Security
The Threat-Model file is the reference for what the forge trusts and refuses to do. Operational checklist:
- Software checks.
deploy/audit.shrunsgo vet,govulncheck(the module list is deliberately short — review it on each release), and a short fuzz pass over every attacker-facing parser (pkt-line, commit, SSHSIG armor, OpenPGP key, SSH tokenizer). Run it before tagging a release. - Web responses carry a scripts-forbidden CSP,
X-Frame-Options: DENY,nosniff,no-referrer, and HSTS when TLS is on — no configuration needed. - Host sandboxing. The systemd unit in
deploy/cloud-init.yamlruns gitbayd unprivileged withProtectSystem=strict,PrivateDevices,LockPersonality,MemoryDenyWriteExecute,SystemCallFilter=@system-service, andRestrictAddressFamiliesto INET/INET6/UNIX. It keepsCAP_NET_BIND_SERVICEonly, to bind 22/80/443. - OS patches apply via
unattended-upgrades(security origins, auto-reboot 04:30 if required). - Admin sshd (2222) is throttled by
MaxStartups=/=MaxAuthTriesand watched byfail2ban; gitbayd's own port 22 is throttled bylimits.ssh_auth_rate(auth failures per IP per minute). - Monitoring.
gitbay-monitor.timerposts disk/service/cert status hourly to the webhook URL in/etc/gitbay/monitor.url(create the file to enable; absent = silent). Alerts fire on a stopped service or disk ≥ 85%. - Database.
gitbay.dband its WAL live under/var/lib/gitbay(mode 0750, owned bygitbay). The nightly archive plus provider snapshots are the recovery path; for tighter RPO, add continuous replication (litestream) against the same file — it coexists with the WAL.
Odds and ends
- deleting a fork marks MRs sourced from it
source_gone; their diffs remain viewable and mergeable because the target repo owns the objects. refs/merge-requests/*is server-owned and unpushable by clients.- audit-relevant activity (issue/MR lifecycle, imports, pushes) lands in
the
eventstable, which also feeds webhooks. - the daemon idles under 10MB RSS; the smallest VPS tier is adequate.