-
Notifications
You must be signed in to change notification settings - Fork 1
Security Model
vibe-coder is a single-user LAN tool. The threat model assumes:
- The host PC is trusted (you own it).
- The LAN is trusted (your home/office network behind a firewall).
- The operator is trusted (you).
- Public internet attackers cannot reach the server (no port forwarding, no public IP exposure).
If any of those assumptions fail, you need a reverse proxy with TLS, a WAF, and a higher-grade auth layer — beyond the scope of vibe-coder MVP.
POST /api/auth/login returns a token. Clients can pass it as
Authorization: Bearer <token> or as a vibe_session cookie. Both paths
converge in installAuth (Ktor plugin).
client → POST /api/auth/login {username, password}
→ 200 {token, deviceId, serverName, username}
← stores token in DataStore / cookie
client → GET /api/projects
Header: Authorization: Bearer <token>
→ 200 [...]
- BCrypt cost 12 (configurable in
server.yml). - Stored as hash only. The plaintext password is never persisted.
- Timing-safe
dummy verifyruns on missing users (prevents enumeration).
Two-layer brute-force defence:
-
Account lock: 10 consecutive
/api/auth/loginfailures lock the account for 15 minutes. - IP block: 30 failures from a single IP within a 24-hour window block that IP for 24 hours. Catches credential-stuffing across multiple accounts (the per-account counter would otherwise reset between targets).
Both counters are in-memory (AuthService.failures / ipFailures); server
restart clears them. The tradeoff is intentional — a server restart is
itself a security-relevant event the operator initiates.
Timing-safe dummy verify runs on missing users so response time matches a real BCrypt verification (~250 ms at cost 12).
Per-IP token bucket in front of /api/, /ws/, and /login
paths. Admin sessions / Bearer tokens bypass. Disabled if
security.rateLimit.enabled = false (handy behind nginx /
Cloudflare). See Rate Limit for the threat model
and metric integration.
webauthn4j 0.29.1 implements registration + assertion verification.
Same-origin signature defeats the phishing class of attacks that TOTP
remains vulnerable to. Both methods are available concurrently — the
login page exposes "🔑 Passkey 로 로그인" next to the password form.
- Registration ceremony at
/webauthn(any authenticated user). The attestation object is persisted whole (base64url CBOR) so the assertion path can rebuild aCredentialRecordvia the 4-argCredentialRecordImpl(AttestationObject, ...)constructor. - Assertion ceremony at
POST /api/webauthn/assert/{options,verify}— unauthenticated (it's the login itself). On success the server mints a fresh device row + Bearer token +vibe_sessioncookie. - Challenge state is in-memory (5-minute TTL). A restart cancels any in-flight ceremony — single-user dev server context.
-
server.webauthn.{rpId, rpName, origin}config must match the hostname users browse to. See WebAuthn (Passkey) for the full setup playbook. - Audit:
auth.passkey.register,auth.passkey.login,auth.passkey.delete.
TOTP (RFC 6238, HMAC-SHA1, 30 s period, 6 digits) — Google Authenticator
compatible, self-implemented (no external dependencies). Enable at
/2fa. When active:
- Client posts
username + passwordto/api/auth/login. - Server returns
401 totp_required(audited as a normal step, not a failure). - Client prompts user for Authenticator code, retries with
totpCodeadded. - Server verifies (window
±1for clock drift), then issues the token.
Wrong code returns 401 invalid_totp and does count against the
brute-force counters. See Two-Factor Auth for the
full enrollment flow + lost-device recovery.
vibe-coder-server is a single-operator tool. There are no user roles,
no /users management UI, and no per-user Project ACL.
- There is exactly one admin (created at
/setup). Every authenticated session — cookie or Bearer — has full access to every endpoint and every project. - The only access boundary is authentication itself: password, 2FA, passkey, idle timeout, and brute-force / IP lockout.
security.sessionIdleTimeoutMinutes (default 30, 0 = unlimited).
Both AuthPlugin (Bearer) and SSR requireSessionOrRedirect check
device.lastSeenAt on every request. If older than the threshold:
- The device row is deleted (token immediately invalid for everyone).
- SSR redirects to
/login?err=session_timeout. - JSON API returns
401 unauthorizedwith no body (Bearer flow has no user-visible message channel). - Audit row:
auth.session.timeout.
Combined with 2FA this gives "phishing-resistant once enrolled, fresh 2FA prompt after each idle period" without a "remember me" cookie.
Every SSR POST carries a hidden _csrf input. The token is derived from
the device cookie via HMAC-SHA256 with a server-side pepper (rotated on
restart). REST API (Bearer header, not cookie) is exempt — Authorization
headers can't be auto-attached by a cross-origin page.
Multipart uploads (/projects/{id}/files/upload, MCP secret file, etc.)
carry _csrf in the query string since receiveParameters is unavailable.
See CsrfTokens.kt. Reject with HTTP 403 + audit log.
WS handshake reads Origin and rejects mismatched hosts as a
defense-in-depth against cross-site WebSocket hijacking — even though
SameSite=Lax already prevents the cookie from attaching cross-site in the
typical browser.
Two paths:
- Visit
/setupin a browser → fill the form. - Set
VIBECODER_ADMIN_USERNAMEandVIBECODER_ADMIN_PASSWORDenv vars before firstdocker compose up -d. The entrypoint hands them to the server; if no admin exists yet, it auto-creates one.
After auto-bootstrap, change the password via /password so the
plaintext doesn't linger in .env.
Every disk read/write goes through PathSafety.normalizeAndCheck, which:
- Resolves
..and symlinks. - Checks the result is a descendant of
WorkspacePath.root(or.vibecoder/). - Rejects with
ApiException(400, "path_traversal", ...)if not.
Direct consequence: a malicious project name or file path cannot escape the workspace. Claude itself, when asked to "edit ../../../etc/passwd", hits the same check and reports failure to the user.
workspace:
uploadDeniedExtensions:
- exe
- bat
- cmd
- ps1
- shConfigurable in server.yml. The default list blocks Windows scripts +
shell scripts that could be accidentally executed by other tools.
Auth happens via the first frame, not URL query:
{ "type": "auth", "token": "<token>" }Sent within 5 s of connection, else server closes. This avoids leaking the token to access logs, proxies, or browser history.
- All Claude / Gradle / Git / npm child processes run as the unprivileged
vibeuser (UID/GID matched to host viaPUID/PGID). -
vibehas NOPASSWDsudoinside the container — but only inside the container. The container itself has no access to the host filesystem except the bind mounts in./vibe-coder-data/. - No raw shell endpoint. There's no
/api/execor web terminal. - The
script -qPTY wrap used for the Claude semi-automatic OAuth exposes only a single one-line text input field (the OAuth code), not a full shell.
- All external commands have hard timeouts (5 min default for short ops, 10 min for git clone, 30 min for builds).
- Cancellation calls
Process.destroyForcibly()afterdestroy()waits 5 s. - Background tasks (build / install / clone) emit
Done(status=…)over WebSocket on completion, including TIMEOUT/CANCELED states. Clients can always tell.
The container listens on 0.0.0.0:17880 inside its network. Compose
maps it to ${VIBE_PORT:-17880} on the host. Do NOT forward this
port to the public internet. If you need remote access:
- VPN into your LAN (Tailscale / WireGuard).
- Or expose only through a reverse proxy with HTTPS termination + auth.
POST /api/webhooks/build/{projectId} is admin-auth-free to allow
external systems (GitHub Actions, CI, monitoring) to fire builds. It
requires three headers instead:
-
X-Vibe-Secret-Id— picks the right row frombuild_webhook_secrets. -
X-Vibe-Secret— plaintext secret transmitted over the wire. Server computesSHA-256(secret)and compares to the stored hash with constant-time equality. -
X-Vibe-Signature— optionalHMAC-SHA256(secret, body)hex. When present and the body is non-empty, the server also verifies. Gives body integrity even when TLS terminates upstream.
The secret value is shown once on /projects/{id}/automation right
after creation (yellow card, persisted only as a SHA-256 hash). Lost
secrets must be rotated by deleting + recreating.
This design transmits the plaintext secret on every call, which means a reverse proxy with HTTPS is mandatory when exposing the webhook outside LAN. The roadmap tracks a stricter HMAC-only mode where the server stores the plaintext encrypted with a server pepper.
WebhookNotifier accepts only three host families:
- Slack:
hooks.slack.com/* - Discord:
discord.com/api/webhooks/*ordiscordapp.com/api/webhooks/* - Telegram:
api.telegram.orgreached via a bot token matching^\d+:[A-Za-z0-9_-]+$
HttpClient is built with Redirect.NEVER so a 30x response can't bounce
the request to an internal host. There is no way to point a webhook
at an arbitrary URL from the /settings/webhook form — by design. If you
need a custom collector, build an adapter that subscribes to the audit
log instead.
There is no in-container emulator, no noVNC websockify gateway, and no
/dev/kvm privileged-container path — so there is no raw VNC port to
secure. To test on a real device use wireless ADB.
/projects/{id}/env-files exposes only a hard-coded whitelist (7 files
including local.properties, .env, build.gradle.kts, etc.). Any
other path returns invalid file:. This prevents the form from being
turned into a path-traversal lever — a user who can edit the form
markup still can't write outside the whitelist because the server
validates rel against the ENV_FILES_WHITELIST constant.
/backup/download streams a tar.gz that includes Claude OAuth
credentials (they live under vibe-coder-data/claude/ which is inside
the workspace tree). The credentials are already encrypted by Claude CLI
at rest, but treat the resulting tar as as sensitive as the
credentials themselves — don't commit it to git, don't put it in a
shared bucket without server-side encryption.
PostgreSQL data dir is excluded (page-tear risk on live dir copy);
use the pg_dump command rendered on the page instead.
Every IAM-level action (logins, password changes, 2FA enable/disable,
session timeout, project create/delete, build queue/cancel, MCP install,
settings save, git token lifecycle, git commit, console new/cancel,
Play/TestFlight upload trigger, agent
save/delete, schedule create/delete, webhook secret create/delete,
external webhook build trigger) is recorded into audit_log with
timestamp, user, IP, result, and an optional JSON detail. View at
/audit (left nav: 감사 로그).
See the Audit Log page for the schema, recorded actions, URL filter recipes, and rotation guidance. Failure to write an audit entry never blocks the user's request — it logs to stderr only.
.env is in .gitignore so it never reaches git, but it sits on disk
in plaintext. Lock its permissions:
chmod 0600 ~/vibe-coder/.envIf you set VIBECODER_ADMIN_PASSWORD here for auto-bootstrap, rotate it
via /password after first login, then either remove it from .env or
accept the risk that someone with file read access can see it.
Open a private security advisory on GitHub. Do not file public issues.