Commit 0ff41e58be
0ff41e58be98ea111521f92f769a9e0d4ad59480
parent: a6f7827f2e
Verified · cmc
cmc <hello@cleberg.net> · 2026-09-28 07:51 UTC
web: sessions list shows last use; docs for the idle timeout
Closes #276
Layout: unified · split
.gitbay/wiki/Architecture/05-Identity-and-Access.org
+2 −1
| @@ -18,7 +18,7 @@ |
| 18 | 18 | | SSH user key | user's public key | fingerprint and public blob | =full=, =git=, or =runner= | optional =--ttl=, refused at auth | =keys remove= (own keys); closes its connections | |
| 19 | 19 | | Deploy key | public key | same table, scope =deploy:<repo>:ro/rw= | one repository, read or read-write | optional =--ttl=, refused at auth | =repo deploy-key remove= (repo admin); closes its connections | |
| 20 | 20 | | API token | =gb_= + 32 random bytes hex | SHA-256 hash | =read= (default) or =full=; with an expiry, no credential-minting command | optional =--ttl= | =token revoke [--created]= | |
| 21 | | | Web session | 32 random bytes hex, cookie =gitbay_session= | SHA-256 hash | full account | 7 days, no sliding renewal | logout, =web sessions revoke= | |
| 21 | | Web session | 32 random bytes hex, cookie =gitbay_session= | SHA-256 hash | full account | 12 h idle, 7 days absolute | logout, =web sessions revoke= | |
| 22 | 22 | | Login link | 32 random bytes hex in a URL | SHA-256 hash, single use | creates a web session | 15 min (mail), 5 min (SSH) | consumed on use | |
| 23 | 23 | | Email verification | 32 random bytes hex | SHA-256 hash, single use | verifies one address for one account | 24 h | consumed on use | |
| 24 | 24 | | Invite | random code | SHA-256 hash, single use | one registration for one email | as issued | consumed on use | |
| @@ -27,6 +27,7 @@ |
| 27 | 27 | Generation and hashing: =internal/store/sessions.go= (=NewToken=, |
| 28 | 28 | =HashToken=, =crypto/rand=). Cookie attributes: =HttpOnly=, |
| 29 | 29 | =SameSite=Lax=, =Secure= unless TLS is off, =MaxAge= 7 days |
| 30 | (the session itself also ends after 12 hours idle) |
| 30 | 31 | (=internal/httpd/accounts.go=). Token scope values are |
| 31 | 32 | constrained by a database =CHECK= as well as the command |
| 32 | 33 | (=internal/store/migrations/0004_api_tokens.up.sql=). |
.gitbay/wiki/Architecture/09-Controls.org
+1 −1
| @@ -23,7 +23,7 @@ chapter names of OWASP ASVS 4.0 where one fits. |
| 23 | 23 | | Brute-force limit on SSH auth | in place | 10 failures a minute per IP (=internal/sshd/ratelimit.go=) | |
| 24 | 24 | | Account enumeration resistance at login | in place | uniform response (=internal/control/loginlink.go=) | |
| 25 | 25 | | Session cookie flags | in place | HttpOnly, SameSite=Lax, Secure with TLS (=internal/httpd/accounts.go=) | |
| 26 | | | Session lifetime | partial | 7 days absolute, no idle timeout (#276) | |
| 26 | | Session lifetime | in place | 12 hours idle, 7 days absolute (=internal/store/sessions.go=) | |
| 27 | 27 | | Credential expiry | in place | optional =--ttl= on API tokens, SSH and deploy keys; checked at auth and per exec | |
| 28 | 28 | | Revocation takes effect immediately | in place | removing a key or disabling an account closes its connections; every exec re-reads its key (=internal/sshd/sshd.go=) | |
| 29 | 29 | | Delegation bounded by the delegating credential | partial | expiring tokens refused on =MintsCredential= commands; credentials record their creating token (=internal/control/control.go=); a web session can still mint credentials that outlive it (#297) | |
.gitbay/wiki/Architecture/10-Known-Gaps.org
−1
| @@ -19,7 +19,6 @@ what the 2026-09-27 review found; remove a row when its issue closes. |
| 19 | 19 | | #273 | Data at rest | CI secrets, webhook secrets and mirror tokens are stored in clear in SQLite | high | |
| 20 | 20 | | #274 | Backups | The local backup archive is not encrypted | medium | |
| 21 | 21 | | #275 | Audit | Refused writes are not audited; the audit table is writable by the daemon user | medium | |
| 22 | | | #276 | Sessions | Web sessions last 7 days with no idle timeout | low | |
| 23 | 22 | | #278 | Login links | =web login= over SSH skips the login-link rate limit | low | |
| 24 | 23 | | #279 | SSRF | Mirror URLs are checked when saved, not when git connects | medium | |
| 25 | 24 | | #280 | Mail | STARTTLS only when the relay offers it | medium | |
.gitbay/wiki/Users.org
+4 −3
| @@ -682,9 +682,10 @@ and deletes through the commands above. |
| 682 | 682 | |
| 683 | 683 | * Browser sessions |
| 684 | 684 | |
| 685 | | =gitbay web login= mints a one-time URL; the session it opens lasts |
| 686 | | seven days. =gitbay web sessions list= shows each of yours by a short |
| 687 | | id with its creation and expiry, and =gitbay web sessions revoke <id>= |
| 685 | =gitbay web login= mints a one-time URL; the session it opens ends |
| 686 | after twelve hours without a request, and after seven days in any case. |
| 687 | =gitbay web sessions list= shows each of yours by a short id with its |
| 688 | creation, expiry and last use, and =gitbay web sessions revoke <id>= |
| 688 | 689 | or =--all= ends them from the terminal, which is where a lost laptop is |
| 689 | 690 | handled. |
| 690 | 691 | |
CHANGELOG.org
+3
| @@ -27,6 +27,9 @@ must add =--scope full=. Existing tokens keep their scope. |
| 27 | 27 | =EXPIRES= columns, after the label (#277). |
| 28 | 28 | - =token list= at a terminal shows a future expiry as a time, not |
| 29 | 29 | "just now" (#286). |
| 30 | - Browser sessions end after twelve hours without a request, and after |
| 31 | seven days as before. Sessions open at upgrade get a fresh twelve |
| 32 | hours. =web sessions list= shows when each was last used (#276). |
| 30 | 33 | |
| 31 | 34 | * v1.36.0 — 2026-09-23 |
| 32 | 35 | |
internal/control/web.go
+2 −2
| @@ -40,7 +40,7 @@ func runWebSessionsList(c *Ctx, args []string) int { |
| 40 | 40 | return c.fail(protocol.ExitFailure, "%v", err) |
| 41 | 41 | } |
| 42 | 42 | return c.emit(sessions, func(w io.Writer) { |
| 43 | | tb := c.table(w, "ID", "SINCE", "UNTIL") |
| 43 | tb := c.table(w, "ID", "SINCE", "UNTIL", "USED") |
| 44 | 44 | for _, s := range sessions { |
| 45 | 45 | since, until := s.CreatedAt, s.ExpiresAt |
| 46 | 46 | if c.Term.Cols == 0 { |
| @@ -48,7 +48,7 @@ func runWebSessionsList(c *Ctx, args []string) int { |
| 48 | 48 | } else { |
| 49 | 49 | since, until = relAge(since, termNow()), relAge(until, termNow()) |
| 50 | 50 | } |
| 51 | | tb.row(cRef(s.ID), cText("since "+since), cText("until "+until)) |
| 51 | tb.row(cRef(s.ID), cText("since "+since), cText("until "+until), cText(c.usedText(s.LastUsedAt))) |
| 52 | 52 | } |
| 53 | 53 | tb.flush() |
| 54 | 54 | }) |
internal/httpd/accounts.go
+2
| @@ -144,6 +144,8 @@ func (s *Server) login(w http.ResponseWriter, r *http.Request) { |
| 144 | 144 | http.Error(w, "internal error", http.StatusInternalServerError) |
| 145 | 145 | return |
| 146 | 146 | } |
| 147 | // Seven days is the cap; the store ends it sooner after |
| 148 | // store.WebSessionIdle without a request. |
| 147 | 149 | if err := s.st.CreateWebSession(sessHash, userID, 7*24*time.Hour); err != nil { |
| 148 | 150 | http.Error(w, "internal error", http.StatusInternalServerError) |
| 149 | 151 | return |