feat: wave 1 — remote attach + security baseline - #19
Merged
Conversation
Wave 1.1 — Accept Basic auth at the manager edge (closes #2) - _authenticated() now accepts Authorization: Basic opencode:<password> in addition to the oc_session cookie, unblocking - ?auth_token=<password> query param fallback for headerless clients - Non-browser clients (Basic auth / auth_token) get 401 instead of 302 to an HTML login form they can't render - auth_token stripped from query before proxying to the child Wave 1.2 — Security headers on all responses (closes #3) - CSP (frame-ancestors 'none', style-src 'unsafe-inline') on manager HTML - X-Frame-Options: DENY, X-Content-Type-Options: nosniff, Referrer-Policy - HSTS only when X-Forwarded-Proto: https - Proxied responses get the safe subset (no CSP on opencode UI) Wave 1.3 — CSRF tokens on all state-changing POSTs (closes #4) - Per-session token: hmac_sha256(session_secret, 'csrf') - Double-submit nonce cookie for login / first-run (no session yet) - All state-changing POSTs verified: login, logout, restart, revalidate, rotate, setup, test-key, test-github, test-mcp - Proxied opencode API POSTs NOT checked (own auth via session gate) - JS fetch calls updated to include csrf_token from the form Wave 1.4 — Fix rate limiter + login lockout (closes #5) - Primary key is socket peer address, not X-Forwarded-For - TRUSTED_XFF_HOPS env var (default 1) controls XFF trust depth - Separate GET/POST buckets so GET traffic doesn't exhaust POST budget - All POST routes covered (was only 6, now includes restart/rotate/etc.) - GET routes rate-limited at 120/60s (health exempt) - Exponential login lockout after 5 failures: 60s → 300s → 900s - Successful login resets the lockout counter Tests: 28 new test cases covering all four slices. 108 total, all passing. Lint: ruff clean.
wra-sol
force-pushed
the
feat/wave-1-attach-and-security-baseline
branch
from
June 29, 2026 02:45
c92bcf3 to
6c8657e
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Wave 1 of the opencode manager improvement plan. Four slices, one PR:
1.1 — Accept Basic auth at the manager edge (closes #2)
The manager's auth gate now accepts
Authorization: Basic opencode:<password>in addition to theoc_sessioncookie. This unblocks the documentedopencode attach https://<app> -p <password>terminal-reconnect feature, which was broken because the manager only accepted cookies and redirected Basic-auth clients to an HTML login form the TUI can't render.Also adds
?auth_token=<password>query-param fallback (matching the opencode server's own feature) and strips it before proxying so the credential doesn't leak to the child's logs.Non-browser clients (Basic auth / auth_token) now get a clean
401 Unauthorizedinstead of a302to an HTML form.1.2 — Security headers on all responses (closes #3)
Content-Security-Policyon manager HTML pages (frame-ancestors 'none',style-src 'unsafe-inline'for the inline CSS)X-Frame-Options: DENY— clickjacking protectionX-Content-Type-Options: nosniff— MIME sniffing protectionReferrer-Policy: same-originStrict-Transport-Security— only whenX-Forwarded-Proto: https(reuses the existing proto detection)1.3 — CSRF tokens on all state-changing POSTs (closes #4)
hmac_sha256(session_secret, "csrf")— stable across requests, rotates on password change/manage/login,/manage/logout,/manage/restart,/manage/revalidate,/manage/keys/rotate,/setup,/test-key,/test-github,/test-mcpopencode attachonly does GETs to the proxied API)SameSite=Strictcookie stays as defense-in-depth1.4 — Fix rate limiter + login lockout (closes #5)
Three fixes:
X-Forwarded-For. The old code trusted XFF unconditionally — an attacker could spoof a new IP per request for unlimited login attempts. NewTRUSTED_XFF_HOPSenv var (default 1 for Railway's single proxy hop) controls how many XFF hops to trust./manage/restart,/manage/revalidate,/manage/keys/rotate). GET routes also rate-limited at 120/60s (health exempt). Separate GET/POST buckets so heavy GET traffic doesn't exhaust the POST budget.Tests
28 new test cases covering all four slices:
All 108 tests pass.
ruff checkclean.Configuration
New optional env var:
TRUSTED_XFF_HOPS(default1) — number of X-Forwarded-For hops to trust for client IP extraction. Set to0to ignore XFF entirely (use socket peer only).