Skip to the content.

Security Architecture

PuTTrY is designed as a single-user terminal server with centralized credential management on the backend. This document details both application-level security (authentication and session management) and web application security (server-side attack mitigations).

1. Introduction

PuTTrY’s security model eliminates the key management burden of SSH by centralizing credentials on the server instead of distributing them across client machines. The architecture is built on layered defenses:

All state (sessions, temporary challenges, credentials) is in-memory and on-disk—there is no external database dependency.


2. Authentication

2.1 Session Password

The session password is PuTTrY’s primary credential. It protects access to the web UI and all API endpoints.

Hashing Algorithm: scrypt

Storage Format

scrypt:<salt_hex>:<hash_hex>

Example: scrypt:a1b2c3d4e5f6...:<64-byte hex hash>

Stored in: ~/.puttry/session-password.txt with file permissions 0o600 (readable only by the owning user).

Verification

Password Length Guard

Generation PuTTrY can generate passwords in two modes:

  1. XKCD-style (default, SESSION_PASSWORD_TYPE=xkcd):
    • Format: word1-word2-word3-word4-digit (e.g., castle-piano-river-seven-3)
    • Selects 4 random words from a ~700-word list + 1 random digit (0-9)
    • Cryptographically secure: uses crypto.getRandomValues()
    • Entropy: ~54 bits of entropy per word (~224 bits total)
    • Memorable and easy to type
  2. Random alphanumeric (SESSION_PASSWORD_TYPE=random):
    • Format: 16 random alphanumeric characters by default (configurable via SESSION_PASSWORD_LENGTH)
    • Cryptographically secure: uses crypto.getRandomValues()
    • Higher entropy per character; suitable for password managers

2.2 TOTP (Time-Based One-Time Password)

TOTP provides a second authentication factor via a time-based token generator (Google Authenticator, Authy, etc.).

Standards & Configuration

Secret Management

Replay Prevention A lastUsedCodes map tracks the most recent valid code per secret:

State File: ~/.puttry/2fa-state.json

{
  "secret": "base32-encoded-secret",
  "verified": true,
  "setupAt": "2026-03-20T14:30:00.000Z"
}

Setup Flow & Expiration

2.3 Passkeys (WebAuthn)

Passkeys provide phishing-resistant cryptographic authentication using your device’s built-in security (Touch ID, Face ID, Windows Hello, or security keys).

Libraries & Standards

Relying Party (RP) Configuration

Challenge Management

Signature Counter Verification

Storage: ~/.puttry/passkeys.json

[
  {
    "id": "base64url-credential-id",
    "name": "iPhone Touch ID",
    "publicKey": "base64-encoded-public-key",
    "counter": 42,
    "registeredAt": "2026-03-20T14:30:00.000Z",
    "transports": ["internal", "hybrid"]
  }
]

Dual Modes

  1. Passkey as 2FA (PASSKEY_AS_2FA=true, default):
    • After password entry, user must authenticate with a passkey
    • Cannot be used alone—password is always required first
    • Second factor can also be TOTP if both are enabled
  2. Passkey as standalone auth (PASSKEY_AS_2FA=false):
    • Passkey replaces password entirely—no password required
    • User selects “Sign in with passkey” and completes WebAuthn authentication
    • Suitable for environments where biometric auth is preferred

2.4 Multi-Factor Login Flow

When the session password is correct, the server checks the 2FA configuration:

Case 1: No 2FA Configured

POST /api/auth/login (password)
  → ✓ Password valid
  → Create browser session (_wt_session)
  → Return { authenticated: true }

Case 2: TOTP or Passkey Required (2FA Active)

POST /api/auth/login (password)
  → ✓ Password valid
  → Create temporary session (_wt_temp)
  → Return { authenticated: false, requiresTOTP: true } OR { requiresPasskey: true }
POST /api/auth/totp/verify (code) OR /api/auth/passkey/verify
  → Verify code/signature
  → Promote _wt_temp to _wt_session (browser session)
  → Return { authenticated: true }

Case 3: Both TOTP and Passkey Enabled

POST /api/auth/login (password)
  → ✓ Password valid
  → Create temporary session (_wt_temp)
  → Return { canChoose: true, requiresTOTP: true, requiresPasskey: true }
User chooses verification method
  → POST /api/auth/totp/verify OR POST /api/auth/passkey/verify
  → Promote _wt_temp to _wt_session

Case 4: TOTP Setup Required (TOTP enabled but not yet configured)

POST /api/auth/login (password)
  → ✓ Password valid
  → No passkeys active, TOTP not yet set up
  → Create temporary session (_wt_temp)
  → Return { requiresTOTP: true, totpMode: "setup" }
User completes TOTP setup
  → QR code displayed (contains secret)
  → POST /api/auth/totp/setup/verify (code)
  → Save TOTP state, promote to browser session

Guest Links provide a secure alternative to password sharing for collaborative access. Instead of exposing the owner’s credentials, the owner creates one-time invite URLs that grant limited, time-bound access.

Link Token Issuance

Guest links are created via POST /api/guest-links (owner only):

Redemption and Session Creation

Guest opens the invite URL and posts to POST /api/guest/redeem:

Comparison: Owner vs. Guest Session Cookies

Property _wt_session (owner) _wt_guest (guest)
Token entropy UUID v4 (122 bit) UUID v4 (122 bit)
TTL 24 hours 4 hours
SameSite Strict Lax
Auth method Password + optional 2FA Invite URL possession
Persistent storage None (in-memory) None (in-memory)

The SameSite=Lax attribute on guest cookies is intentional: it allows the guest to be logged in when they click the invite link from email or a message. This is a reasonable trade-off because (1) guests have read-only access by default, and (2) the invite URL is the single credential—sharing it only grants observation rights.

Access Control Matrix

Capability Owner Guest
View terminal output ✓ (read-only)
Send terminal input Only after owner approval
Take/hold write lock Only after owner approval
Read config & settings
Change settings
Access file manager
Create / revoke guest links
Approve / deny control requests

Write Lock and Control Request Flow

Guests are read-only by default. To request write access:

  1. Guest clicks a terminal session and selects “Request Control” (or opens a new terminal connection in guest mode)
  2. Guest client sends { type: 'request-lock' } over the terminal WebSocket
  3. Server creates a LockRequest entry in-memory with a 30-second timeout
  4. Owner receives a notification and a dialog asking to approve or deny
  5. Approval (POST /api/guest/lock-requests/:id/approve):
    • Guest is granted the write lock for that session’s sessionId
    • Input from the guest’s clientId is now accepted by the PTY
  6. Denial (POST /api/guest/lock-requests/:id/deny):
    • Request is deleted; guest returns to read-only mode
  7. Auto-expiry (after 30 seconds):
    • If neither approval nor denial, the request is automatically removed
    • lock-request-expired is broadcast to all clients

Revocation and Immediate Disconnection

Owner can revoke guest access at any time:

No grace period — disconnection is immediate and unavoidable.

Session Expiry

Guest sessions expire automatically after 4 hours:


3. Session Management

PuTTrY uses two types of cookies to manage authentication state:

Cookie Purpose TTL Attributes
_wt_session Full authentication session (owner) 24 hours HttpOnly, SameSite=Strict, Path=/
_wt_temp 2FA in-progress (temporary) 5 minutes HttpOnly, SameSite=Strict, Path=/
_wt_guest Guest session authentication 4 hours HttpOnly, SameSite=Lax, Path=/

Cookie Attributes

Session Tokens

Session Cleanup

Logout


4. Web Application Security

4.1 HTTP Security Headers

PuTTrY sets the following headers on all responses:

Header Value Protection
X-Frame-Options DENY Clickjacking (prevents embedding in <iframe>)
X-Content-Type-Options nosniff MIME sniffing attacks (enforces declared content type)
Referrer-Policy strict-origin-when-cross-origin Referrer leakage (sends full referrer to same origin, none to cross-origin)
Content-Security-Policy (see below) XSS, resource injection

Content Security Policy (CSP)

Production CSP:

default-src 'self';
connect-src 'self' ws: wss:;
img-src 'self' data:;
script-src 'self';
style-src 'self' 'unsafe-inline'

Development CSP adds 'unsafe-inline' to script-src to support Vite HMR during development.

4.2 DNS Rebinding Protection

DNS rebinding attacks occur when an attacker controls a domain that resolves to the victim’s localhost/private IP, allowing cross-origin requests to the victim’s server.

Mitigation

Example:

# Allow requests from localhost and puttry.example.com
ALLOWED_HOSTS=localhost,puttry.example.com

4.3 CSRF Mitigation

Cross-Site Request Forgery (CSRF) attacks trick browsers into sending authenticated requests to a victim’s server from an attacker’s site.

Primary Defense: SameSite=Strict Cookies

Logout Protection

4.4 Rate Limiting

Four independent rate limiters protect against brute-force and denial-of-service attacks:

Limiter Endpoint(s) Window Max Requests Purpose
Global All unauthenticated 15 min 500 DoS protection (skipped for authenticated users)
Password Login POST /api/auth/login 1 hour 10 Brute-force protection
2FA Verify POST /api/auth/totp/verify, /api/auth/passkey/verify 10 min 5 2FA brute-force protection
Passkey Challenge POST /api/auth/passkey/standalone/options 15 min 10 Passkey setup DoS prevention

Configuration

Rate Limit Security

4.5 WebSocket Authentication and Revalidation

WebSocket connections (used for terminal I/O and sync messages) require authentication and ongoing validation.

Initial Authentication (Upgrade Time)

Guest WebSocket Restrictions

Periodic Revalidation

Two WebSocket Channels

  1. /sync WebSocket (sync bus):
    • Single persistent connection per browser for coordination
    • Carries control messages (session CRUD, input lock changes)
    • Broadcasts state to all open tabs
    • Max payload: 256 KB
  2. /terminal/:sessionId WebSocket (per-terminal):
    • Individual connections for each viewing terminal
    • Carries raw PTY input/output and resize messages
    • Closed when user switches to a different terminal
    • Max payload: 1 MB

4.6 WebSocket Payload Limits

Oversized payloads are rejected at the WebSocket server level, preventing memory exhaustion attacks:

Channel Max Payload
/sync 256 KB
/terminal/:sessionId 1 MB

4.7 PTY Input Security

Pseudo-terminal input is heavily constrained to prevent abuse:

Input Size Limit

Terminal Resize Security

Shell Invocation (Command Injection Prevention)

Write Lock (Single Writer)

4.8 Configuration and Environment Variable Security

Environment Variable Allowlist (CRIT-6) Only 17 approved environment variables are loaded from .env files:

  1. AUTH_DISABLED
  2. SHOW_AUTH_DISABLED_WARNING
  3. TOTP_ENABLED
  4. SESSION_PASSWORD_TYPE
  5. SESSION_PASSWORD_LENGTH
  6. PASSKEY_RP_ORIGIN
  7. PASSKEY_AS_2FA
  8. RATE_LIMIT_GLOBAL_MAX
  9. RATE_LIMIT_SESSION_PASSWORD_MAX
  10. RATE_LIMIT_TOTP_MAX
  11. RATE_LIMIT_PASSKEY_CHALLENGE_MAX
  12. SCROLLBACK_LINES
  13. PORT
  14. HOST
  15. NODE_ENV
  16. ALLOWED_HOSTS

Any other keys (including dangerous ones like NODE_OPTIONS, LD_PRELOAD, PATH) are silently ignored.

Settings API Restrictions (CRIT-1, HIGH-1)

Settings Value Sanitization (CRIT-2)

Process Environment Isolation


5. On-Disk Security

State files are stored in ~/.puttry/ with strict permissions and validation:

File Permissions Contents Validation
session-password.txt 0o600 scrypt hash (scrypt:<salt>:<hash>) Verified format on load
2fa-state.json 0o600 TOTP secret + status Strict schema validation
passkeys.json 0o600 Array of passkey objects Strict schema validation per entry

Schema Validation

File Permissions


6. What PuTTrY Does Not Handle (Intentionally)

PuTTrY assumes responsibility for application-level security but delegates infrastructure concerns to the deployment layer:

Transport Security: TLS/HTTPS

HTTP Strict Transport Security (HSTS)

Cross-Origin Resource Sharing (CORS)

Multi-User Isolation


7. Threat Model & Mitigations

Threat Attack Method Mitigation
Brute-force Password Repeated login attempts Rate limiting (10 attempts/hour)
Brute-force 2FA Repeated TOTP/passkey attempts Rate limiting (5 attempts/10 min)
Phishing User tricks into revealing password Passkeys are phishing-resistant (RP ID validation)
Credential Stuffing Compromised credentials from other services Password not from dictionary; TOTP/passkey required
Session Hijacking Attacker steals session cookie HttpOnly prevents JavaScript access; SameSite=Strict prevents cross-site leakage
CSRF Cross-site request forgery SameSite=Strict cookies; critical ops require authentication
XSS Injected malicious script Content-Security-Policy (no inline scripts, same-origin only); X-Content-Type-Options: nosniff
Clickjacking Embedding PuTTrY in malicious iframe X-Frame-Options: DENY
DNS Rebinding Attacker’s domain resolves to victim’s IP Host header validation; default allowlist (localhost, 127.0.0.1, ::1)
Man-in-the-Middle (MITM) Attacker intercepts unencrypted traffic HTTPS enforcement at proxy layer; Secure cookie flag
Local Credential Theft Attacker with local file access File permissions 0o600; scrypt hashing (slow, high memory)
Replay Attack (TOTP) Attacker reuses valid TOTP code Replay prevention: same code rejected within 30-second window
Cloned Passkey Attacker clones a registered passkey Signature counter verification detects replayed credentials
DoS via Large Input Sending gigabyte-sized payloads Payload limits: 64 KB PTY input, 256 KB sync, 1 MB terminal
DoS via Malformed Resize Invalid terminal dimensions crash PTY Dimensions clamped to [1, 500]
Command Injection Shell escapes via malformed input No shell interpolation; array-based spawn
Privilege Escalation (Local) Attacker exploits SUID/file perms Files use 0o600; server runs as unprivileged user
Process Injection Attacker modifies process env at runtime Env var allowlist; .env does not override existing process.env
Log Injection Attacker injects log control codes clientId sanitized to [a-zA-Z0-9\-_]*
Settings Tampering Attacker changes security settings via API AUTH_DISABLED and rate limits are CLI-only (not API-accessible)
Newline Injection Attacker injects new .env entries Settings sanitization: \n, \r, \0 stripped before writing
Guest Link Enumeration Attacker guesses or brute-forces invite tokens 256-bit random token (64 hex chars); 20 redemption attempts per 15 min per IP
Guest Token Theft Attacker steals _wt_guest cookie HttpOnly prevents JavaScript access; Secure flag enforces HTTPS
Guest Link Reuse Attacker attempts to redeem a link twice One-time use enforcement: usedAt timestamp permanently invalidates link on first redemption
Unauthorized Guest Input Guest bypasses read-only mode to type Write lock requires explicit owner approval via lock-request flow; 30-second auto-expiry prevents indefinite lockout
Guest Overstay Guest session persists longer than intended Owner can revoke instantly; 4-hour hard TTL enforced in-memory and via cookie Max-Age

8. Verification Checklist

When reviewing PuTTrY’s security:


9. References