Last updated: 2026-04-12 Status: Launch-ready after E2–E5 and H1 fixes.
This document describes the threat model, security invariants, and intentional
design decisions. It is kept deliberately short. If you are looking for the
raw audit trail, see docs/SECURITY-DEEP-AUDIT-2026-04-12.md and
docs/PENTEST-RESULTS-2026-04-12.md.
We protect against the following attackers:
| Attacker | Protected against |
|---|---|
| Same-host user-level process (malware, sandboxed app) | Yes |
| Same-host malicious AI agent (MCP client, npm postinstall) | Yes |
| Passive network observer on the LAN/ISP path | Yes |
| Active network MITM with a rogue CA | Yes (rustls + WebPKI) |
| Someone who gets physical root on the user's machine | No (out of scope) |
| Rails/Sentinel backend compromise | No (out of scope) |
-
Refresh tokens live in the OS keychain only. State files never contain
refresh_token. Seecli/src/state.rs::load()for the migration path from legacy state files. Enforced via#[serde(skip_serializing)]on the field. -
State file mode is 0600. Enforced at every write via
save()andsave_critical(). Verified by tests. -
/tmp/tytus/is 0700 and every file in it is 0600. Enforced viasecure_tytus_tmp_dir()+secure_chmod_600()helpers called at every write site (CLI, tray, daemon, tunnel helper). -
WireGuard private keys never touch disk. The tunnel config is parsed into an in-memory
TunnelConfigstruct and handed to boringtun directly.WireGuardConfigandWannolotPassResponseimplementZeroize. -
Sudoers is tightly scoped. The entry grants exactly two commands:
/Users/USER/bin/tytus tunnel-up /tmp/tytus/tunnel-*.json /Users/USER/bin/tytus tunnel-down *The
tunnel-downhelper validates the target PID against/tmp/tytus/tunnel-*.pidfiles before signalling, so it cannot be used as an arbitrarykillprimitive. Thetunnel-uppath pattern prevents pointing the helper at/etc/shadowor an attacker-controlled config. -
TLS is rustls + WebPKI roots, no
native-tls, no plaintext fallback. Everyreqwest::Clientin the tree goes throughatomek-core::HttpClientor is audited for the same TLS config. -
MCP tools return stable values only by default.
tytus_env,tytus_status, and the daemon socket all emitstable_ai_endpoint(http://10.42.42.1:18080) andstable_user_key(sk-tytus-user-<32hex>) by default. Internal pod IPs and per-pod ephemeral keys are opt-in via--raw/raw=true.
auth/src/login.rs and auth/src/sentinel.rs both contain:
const PUBLIC_CLIENT_API_KEY: &str = "2qQaEiyjeqd0F141C6cFeqpJ353Y7USl";This is the Rails Api-Key header value. It is intentionally public and
is used to identify "this request is coming from the Tytus CLI" for
telemetry, per-client rate limiting, and feature flagging. It is shipped in
every public binary, exactly like:
- Firebase Web SDK API keys (hardcoded into every web app)
- Auth0
client_idvalues (public JavaScript config) - Stripe publishable keys (
pk_live_*— in every e-commerce frontend)
Why this is safe: every endpoint that consumes this value also requires user credentials on top of it:
| Endpoint | Additional required credential |
|---|---|
/ma-authentication-ms/v1/api/auth/login |
email + password in body |
/ma-authentication-ms/v1/api/auth/refresh |
refresh_token in body |
/ma-metrics-wsp-ms/v1/api/me/wannolot-pass |
user OAuth Bearer in header |
An attacker who extracts this key from the binary gains exactly the same access surface as a user who downloads the CLI: none, until they supply their own credentials. The key is metadata, not a gatekeeper.
Invariant this depends on: the Rails API must never add an endpoint that
treats Api-Key as a standalone credential. If it does, this value becomes
a leaked secret, not a public client ID. That would be a Rails-side
regression — catch it during Rails code review, not CLI review.
Not rotatable without breaking every installed binary. If we ever need to rotate it, we must coordinate a forced upgrade of every deployed client, and the old value must remain valid for the full deprecation window.
The tunnel-up helper runs as root for the lifetime of the tunnel (hours to
days). It needs root only briefly: TUN device creation + route setup. In
principle it should drop privileges after that. We currently don't. The
attack surface is limited because:
- The binary is tightly scoped (no shell, no file writes outside
/tmp/tytus) - The sudoers entry is wildcard-free (
tunnel-up /tmp/tytus/tunnel-*.json) - PID validation prevents misuse of
tunnel-down
Lowering the privilege drop is tracked as M3 in the deep audit; it is post-launch work.
The one-liner install flow (curl -fsSL https://get.traylinx.com/install.sh | bash)
is safe to post publicly because:
-
SHA256 verification is mandatory. The installer downloads
SHA256SUMSfrom the release and refuses to install if any binary's hash doesn't match. Escape hatch:TYTUS_SKIP_CHECKSUM=1(not recommended). -
The GitHub release workflow emits
SHA256SUMSfor every artifact. See.github/workflows/release.yml. -
Homebrew, Windows PowerShell, and direct-curl paths all verify.
What this does NOT protect against:
- Compromise of the GitHub account publishing releases. (Mitigation: protected branch rules + required reviews on the release workflow + hardware MFA.)
- Compromise of the Cloudflare Pages static host serving the landing page.
(Mitigation: install script is also mirrored on
raw.githubusercontent.com.)
A future version will add cosign signing of the SHA256SUMS file + keyless verification in the installer; this is tracked as post-launch hardening.
Email security@traylinx.com. Please do not open public GitHub issues for
security findings.
docs/DEEP-AUDIT-2026-04-03.md— first audit (pre-CLI pivot)docs/SECURITY-HARDENING-2026-04-12.md— network/infra sweep + CLI output redactiondocs/SECURITY-DEEP-AUDIT-2026-04-12.md— 34 findings, 1 CRITICAL, 5 HIGHdocs/PENTEST-RESULTS-2026-04-12.md— red team exploitation proofdocs/SECURITY.md(this file) — steady-state model