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 @@
1818| 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 |
1919| 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 |
2020| 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= |
2222| 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 |
2323| Email verification | 32 random bytes hex | SHA-256 hash, single use | verifies one address for one account | 24 h | consumed on use |
2424| Invite | random code | SHA-256 hash, single use | one registration for one email | as issued | consumed on use |
@@ -27,6 +27,7 @@
2727Generation and hashing: =internal/store/sessions.go= (=NewToken=,
2828=HashToken=, =crypto/rand=). Cookie attributes: =HttpOnly=,
2929=SameSite=Lax=, =Secure= unless TLS is off, =MaxAge= 7 days
30(the session itself also ends after 12 hours idle)
3031(=internal/httpd/accounts.go=). Token scope values are
3132constrained by a database =CHECK= as well as the command
3233(=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.
2323| Brute-force limit on SSH auth | in place | 10 failures a minute per IP (=internal/sshd/ratelimit.go=) |
2424| Account enumeration resistance at login | in place | uniform response (=internal/control/loginlink.go=) |
2525| 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=) |
2727| Credential expiry | in place | optional =--ttl= on API tokens, SSH and deploy keys; checked at auth and per exec |
2828| 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=) |
2929| 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.
1919| #273 | Data at rest | CI secrets, webhook secrets and mirror tokens are stored in clear in SQLite | high |
2020| #274 | Backups | The local backup archive is not encrypted | medium |
2121| #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 |
2322| #278 | Login links | =web login= over SSH skips the login-link rate limit | low |
2423| #279 | SSRF | Mirror URLs are checked when saved, not when git connects | medium |
2524| #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.
682682
683683* Browser sessions
684684
685=gitbay web login= mints a one-time URL; the session it opens lasts
686seven days. =gitbay web sessions list= shows each of yours by a short
687id 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
686after 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
688creation, expiry and last use, and =gitbay web sessions revoke <id>=
688689or =--all= ends them from the terminal, which is where a lost laptop is
689690handled.
690691
CHANGELOG.org +3
@@ -27,6 +27,9 @@ must add =--scope full=. Existing tokens keep their scope.
2727 =EXPIRES= columns, after the label (#277).
2828- =token list= at a terminal shows a future expiry as a time, not
2929 "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).
3033
3134* v1.36.0 — 2026-09-23
3235
internal/control/web.go +2 −2
@@ -40,7 +40,7 @@ func runWebSessionsList(c *Ctx, args []string) int {
4040 return c.fail(protocol.ExitFailure, "%v", err)
4141 }
4242 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")
4444 for _, s := range sessions {
4545 since, until := s.CreatedAt, s.ExpiresAt
4646 if c.Term.Cols == 0 {
@@ -48,7 +48,7 @@ func runWebSessionsList(c *Ctx, args []string) int {
4848 } else {
4949 since, until = relAge(since, termNow()), relAge(until, termNow())
5050 }
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)))
5252 }
5353 tb.flush()
5454 })
internal/httpd/accounts.go +2
@@ -144,6 +144,8 @@ func (s *Server) login(w http.ResponseWriter, r *http.Request) {
144144 http.Error(w, "internal error", http.StatusInternalServerError)
145145 return
146146 }
147 // Seven days is the cap; the store ends it sooner after
148 // store.WebSessionIdle without a request.
147149 if err := s.st.CreateWebSession(sessHash, userID, 7*24*time.Hour); err != nil {
148150 http.Error(w, "internal error", http.StatusInternalServerError)
149151 return