This document describes the security primitives PawFlow relies on for
multi-user / public deployments. Every claim here is backed by code in
core/ and services/ and by tests under tests/test_*_security_*.
If you find a divergence, the code wins.
Please do not open a public GitHub issue for security vulnerabilities. Use either of the following private channels:
- GitHub Security Advisories — preferred. Open a private advisory at https://github.com/allcolor/PawFlow-Agents/security/advisories/new.
- Email — pawflow-support@allcolor.org.
Include reproduction steps, affected version (pawflow --version), and any
proof-of-concept. We aim to acknowledge reports within 72 hours.
Internet ─→ [private gateway] ─→ [auth: cookie / API key] ─→ [capability tokens] ─→ resource
public-mode bouncer SecurityManager session per-route signed bind
Each layer is independent: removing one downgrades the deployment but does not silently weaken the others.
The shared HTTP listener adds security headers to every normal response:
Content-Security-Policy, X-Content-Type-Options, X-Frame-Options,
Referrer-Policy, and Permissions-Policy. The CSP remains compatible with
PawFlow's current inline webchat scripts/styles while blocking object embeds
and restricting frame ancestors to same-origin.
A process-local sliding-window limiter protects login/gateway routes and API routes before route dispatch. This is separate from capability-token failure limits: capability limits protect guessed resource URLs, while the listener limit protects global login/API pressure.
- Module:
core/capability_auth.py(storage),core/capability_routes.py(HTTP/WS adapters). - Issued at session-register time (
register_session,register_terminal,register_code_server,add_forward,register_audio_source). - Bound to
(resource_type, resource_id, user_id, login_session_id), and alsoconversation_idwhen the caller supplies one. Browser routes do not rely on the URL carrying a conversation id; their effective boundary is user + login session + resource + token. A token minted forvncdoes NOT verify againstterminal, even with the same resource_id. - Persisted in
data/runtime/capabilities.json(atomic JSON store; SQLite was avoided because WSL/SMB byte-range locking was unreliable). Active VNC / terminal / code-server / port-forward sessions survive a server restart — the same URL keeps working. - TTL bound to the user's login session by default.
SecurityManager.logoutcallsrevoke_session_capabilities(session_id)so a leaked URL stops working the moment the user logs out. - Per-IP rate limit (20 verify failures / 60s) on bad tokens.
- All sensitive URLs follow the shape
/<resource>/<resource_id>/<token>/....
- The
read_onlypermission mode is an allowlist, not a blocklist: any tool not classified as read-only safe is denied. New tools are denied by default until classified explicitly. - The classification lives on
ToolApprovalGate.is_read_only_allowedso the relay and other gates share one source of truth.
- Default:
defaultmode — EXEMPT tools auto-approve, ALWAYS_ASK tools always prompt, anything in between asks once per session. - Production: when the SSE dialog cannot be shown,
ToolApprovalGate.checkreturnsdenied(fail-closed). In development,PAWFLOW_APPROVAL_FAIL_OPEN=truecan be used to allow non-critical tools during UI/event-bus debugging. ALWAYS_ASK tools (bash, store_secret, screen, ...) stay denied even when fail-open is on.
- Format:
enc:v2:<base64>(string) orb"PFSEC2\0" + payload(sidecar bytes). AEAD viacryptography.AESGCM(default) orChaCha20Poly1305. - Master key resolution:
PAWFLOW_SECRET_KEY_B64(raw 32 bytes, preferred) →PAWFLOW_SECRET_KEY(password, scrypt-derived) → generateddata/config/secret.key(chmod 0600, dev only — production refuses to boot with this fallback). - Key rotation:
add_key(kid, key)thenset_current(kid)writes subsequent payloads under the selected kid. - Failure mode: AEAD-auth failure raises
SecretDecryptError. PawFlow never silently returns the ciphertext as a fallback.
Set either PAWFLOW_ENV=production or PAWFLOW_PUBLIC_MODE=true. At
boot, core.security_report.enforce will:
- Print a security snapshot to the log.
- Refuse to boot if any of these is true:
- master key falls back to the on-disk
data/config/secret.key, PAWFLOW_APPROVAL_FAIL_OPENis set,
- master key falls back to the on-disk
- Emit a warning if the HTTP listener binds to
0.0.0.0(so the operator must confirm the firewall / reverse proxy posture).
| Attacker | Defended by |
|---|---|
| Anonymous internet peer | private gateway cookie (_pf_gw) + auth |
| Authenticated user A snooping user B | per-route capability tokens (resource_type + user_id + login-session/resource binding; conversation binding when available) |
| Authenticated user A re-using their own URL on user B's resource | capability mismatch — verify rejects 403 |
| Brute-force token guesser | 256-bit random token + per-IP rate limit |
| Compromised user session (stolen cookie) | capabilities issued for that login session expire / revoke at logout |
| Compromised secrets file | AEAD authenticated decrypt; tampering is detected |
| Untrusted tool author | read_only mode (allowlist), approval gate (always-ask for bash/screen/store_secret/…) |
| Operator misconfiguration in prod | startup security report blocks the boot on weak-key / fail-open |
| Resource | URL shape | Backing store |
|---|---|---|
| VNC | /vnc/<session_id>/<token>/... |
services/vnc_proxy.py |
| Audio | /audio/<session_id>/<token>/stream |
services/audio_proxy.py |
| Terminal | /terminal/<session_id>/<token> |
services/terminal_proxy.py |
| Code-Server | /code/<session_id>/<token>/... |
services/code_server_proxy.py |
| Port-Forward | /fwd/<forward_id>/<token>/... |
services/port_forward_proxy.py |
| Tool relay (WS) | /ws/tools/_tool_relay |
services/tool_relay_service.py |
tests/test_capability_auth.py— issue / verify / revoke / persistence / rate limit.tests/test_route_security_matrix.py— cross-user, expired, revoked, forge-cross-resource.tests/test_tool_approval_phase6.py— read_only allowlist + fail-closed approval.tests/test_secrets_v2.py— AEAD roundtrip, key rotation, tamper detection.tests/test_security_report.py— production-boot policy.tests/test_user_services.py— end-to-end resource lifecycle.
Run the full security suite:
python -m pytest tests/test_capability_auth.py \
tests/test_route_security_matrix.py \
tests/test_tool_approval_phase6.py \
tests/test_secrets_v2.py \
tests/test_security_report.py