Skip to content

Penetration-test plan

Status: Plan only. Nothing here has been run. Driven by threat-model.md: each test names the boundary (TB) it attacks, and a result that contradicts the model’s “Mitigation” column is a finding. Tracked in #66.

  • Test only the dev stack (deploy/compose/setup.sh) or a deployment you own. Never someone else’s compose project or a shared database.
  • Use the dev CA with curl --cacert deploy/compose/.state/internal-ca.pem. Do not bypass certificate warnings or change a trust store.
  • Dev users come from deploy/compose/config/dev-users.toml; use limen_testkit::users in automated tests.
  • Run the workers and targets in compose. Anything that needs real AWS, Okta, or a real CI provider is out of scope here (see the Verify Phase N issues).
  • File one Bug per confirmed finding, labelled security, with the boundary, the steps, and the expected versus observed result. Report a vulnerability that works against a released build privately first.
  • Stop on any sign of impact outside the dev stack (traffic leaving the machine, a changed host setting).

If time is short, run these in order. Each one, if it fails, gives an attacker the keys or hides the evidence.

  1. Certificate issuance: get a certificate for another user or role (token replay, key swap, group-mapping edge cases, bot/person confusion).
  2. Grant and policy: open a resource the role does not allow (forged or replayed grant, label or name look-alikes, stale snapshot, a role or database other than the granted one).
  3. Fabric trust: act as a proxy or worker (join token reuse, header spoofing, a worker certificate on proxy-only calls).
  4. Protocol checks: slip a denied command past the worker (framing tricks, pipelining, encoded paths, /k8s/ identity headers).
  5. Audit integrity: act without leaving an event, or alter one afterwards (spool tampering, full disk, recording swap).
  6. Console: CSRF, XSS, and cross-user reads.
  7. Deployment config: over-broad IAM, KMS, and bucket policies, and exposed admin ports.

Tools: curl/openssl s_client and the limen CLI for 1 to 3, cargo-fuzz and hand-built frames for 4, limen-audit-verify for 5, a browser proxy (own profile, dev CA) for 6.

Each row is a test case. “Expect” is what the threat model promises. Rows marked † go beyond what the model covers today: a failure is a finding and a threat-model gap.

Test Expect
Replay an ID token, an authorization code, and a device-flow token Second use refused (nonce and jti single-use)
Swap the public key in the cert request after login Refused: the nonce binds the key hash
Tamper with state, drop PKCE, downgrade to plain Refused
Forge a bot token: wrong issuer, audience, alg: none, key confusion, expired, far-future iat Refused; no certificate issued
Log in as bot: names from a person’s IdP name Refused (reserved prefix)
† Poisoned OIDC discovery or JWKS: issuer-URL tricks (SSRF to internal hosts), kid confusion, key rotation mid-login, JWKS fetch failure Only the configured issuer is fetched; fails closed
† Name and group edge cases: case and Unicode look-alikes, limen:// or bot: inside names, huge or duplicate group claims One identity per user; no role or SAN injected from a claim
† Bot tokens from forks, pull-request refs, or other branches that match a broad repository or ref rule Too-broad matchers rejected by policy validation; denied login audited
Exceed the login rate limit from one IP; many IPs with X-Forwarded-For Limited per peer IP; header ignored
Search all logs for tokens, cookies, and keys after the above None found (Secret<T>; log scanner test)

Tunnel enrollment and the fabric (TB2, TB9)

Section titled “Tunnel enrollment and the fabric (TB2, TB9)”
Test Expect
Reuse, race, and expire a join token; use a worker token to request a proxy DNS name Single use, atomic; kind-scoped
Present a worker certificate to proxy-only calls (forwarding, peering, AuditHandoff) Refused
Send x-limen-*, Impersonate-*, X-Remote-* headers from a client Stripped or refused
Forge, replay, and expire a session grant; alter the worker name or session id Refused (signature, 60 s TTL, nonce, binding)
Roll back to an older policy snapshot; stop the control plane past the grace period Rollback refused; fail closed after grace
Claim labels not in the worker certificate Ignored; routing uses certified labels only
† Evict the grant nonce cache with many grants, then replay one inside its 60 s life Refused (grant bound to session and worker)
† Enrollment CSR with extra SANs, extensions, or another component’s name; name squatting with a static token Identity comes from the control plane, not the CSR
† Relay loops and open relay: proxy-to-proxy hops that bounce, asking a proxy to reach another worker’s tunnel Hop limit; peer ID pinned from the registry
Test Expect
TLS 1.2, bad ALPN, no client cert, expired or other-CA cert Refused
Slowloris, handshake floods, half-open connections, many streams per tunnel Pre-auth limits and caps hold; the proxy stays responsive
Oversize, truncated, and unknown-field SessionRequest and gRPC messages Rejected with no panic
Use another user’s certificate after limen admin lock or kill-session Refused at login, issuance, proxy, and worker
† HTTP/2 and gRPC abuse before auth: rapid reset, header and metadata floods, CONTINUATION floods, slow reads Bounded; the proxy stays responsive
† Wrong ALPN to reach another service; HTTP/1.1 without ALPN Routed only to what the certificate and ALPN allow
Test Expect
Open each resource kind with a role that lacks it; wildcard and deny-rule edge cases; label and name look-alikes (case, Unicode, trailing dots) Denied; deny wins
Ask for a role, database, login, or namespace other than the granted one in each protocol Refused at the worker
Race a policy change against open sessions Next check applies the change
† Lock or kill a user while a session is opening and during a policy refresh Session ends; no window that opens a new one
Test Expect
Edit, delete, reorder, and truncate spool files and shipped batches; then run limen-audit-verify (runbook) Chain break detected
Fill the spool and the disk; kill components mid-write Fail closed; no silent loss; seq gaps visible
Modify a recording, then play it in the console Hash mismatch refused
Put secrets in statements, headers, and bodies; inspect audit and recordings Matches the redaction rules per protocol
† Emit events as another producer, user, or session from a worker; control characters and newlines in fields Attribution comes from the certificate; control characters stripped
† Delete the newest batches and manifests, then verify Detected, or recorded as a gap (a chain alone cannot show a missing tail without an anchor)
† Recording upload with ../, another session’s id, or a second upload Refused; one write per session
Test Expect
POST without the CSRF token, with a wrong Origin/Referer, cross-site Refused
Stored and reflected XSS through usernames, resource names, labels, session text, and recording content Escaped; CSP blocks inline script
Read other users’ sessions and recordings by id; reach admin pages as a non-admin 404 and 403, enforced server side
Fix, steal, and replay cookies; use a cookie after lock or logout Rejected
† Open redirect and Host-header tricks on login callback, handoff, and links Only the configured public origin is used
† SQL or filter injection and unbounded paging in session, audit, and recording search Literal SQL; limits capped
† Terminal escape sequences (OSC title, clipboard) and HTML in played-back recordings Rendered as data; no script, no clipboard write

Fuzz, then hand-craft hostile input, for every parser that reads bytes from a client or target: PostgreSQL and MySQL frames, RESP, BSON and OP_MSG, AMQP, Kafka, the HTTP paths for search, Kubernetes (/k8s/, upgrade handling, encoded paths), web apps, and SSH. Expect no panic, bounded memory, no desynchronization that lets a command skip the check, and the frame caps in the threat model.

  • † Use each protocol’s dangerous features through an allowed role; expect denial unless granted:
    • SQL: COPY ... PROGRAM, pg_read_server_files, lo_import, SET ROLE, LOAD DATA LOCAL INFILE, INTO OUTFILE.
    • Redis: EVAL, MODULE LOAD, REPLICAOF, MIGRATE, CONFIG SET, DEBUG.
    • MongoDB: $where, $function, $out/$merge, another session’s cursor.
    • Search: scripts, remote _reindex, _snapshot, _tasks.
    • AMQP and Kafka: admin operations, other users’ topics, queues, and groups.
    • SSH: port, agent, and X11 forwarding, env, the sftp subsystem.
    • Kubernetes: pods/proxy, nodes/proxy, other namespaces, portforward to any port.
  • Fuzzing with cargo-fuzz is promised in ADR-0012 and design §13 but is not in the repository yet. Add targets for the MySQL codec, the PostgreSQL frame splitter, and the BSON reader first.
Test Expect
Send Authorization, Impersonate-*, X-Remote-*, X-Limen-* to /k8s/<cluster>/ Refused or stripped; the user comes from the certificate
Reach other verbs through exec/attach/portforward upgrades; encoded paths like %73ecrets Refused
Exec without a recordable stream (base64.* subprotocol, extensions) Refused or recorded
Test Expect
Replay a handoff at another host or resource; open a WebSocket with a foreign Origin Refused
Spoof Host, SNI, and :authority; set identity headers in requests Mismatch refused; headers stripped
Ask limen tcp for an address other than the resource’s Worker connects only to the fixed address
† Request smuggling between proxy, worker, and upstream (CL/TE, HTTP/2 downgrade); header injection through paths One request in, one request out; invalid framing refused
Test Expect
† Another local user races to bind the loopback port, or connects to the listener or --socket Known risk; socket mode 0600, loopback only
† Symlink and race attacks on ~/.limen, detached-connection state and log files 0600, symlinks not followed
† Resource names or labels with newlines or spaces reaching limen config ssh, kubeconfig, or known_hosts Rejected by name validation
† Installer and update path (deploy/cli): piped shell, unsigned downloads Checksum or signature verified

Deployment and cloud config (review, no live attack)

Section titled “Deployment and cloud config (review, no live attack)”

Read the Terraform, Helm, and ECS/EC2 definitions as an attacker would: worker role limited to dbuser:<id>/limen_*; kms:Sign only for the control plane with the algorithm condition and one key per purpose; S3 bucket policy denies delete, uses SSE-KMS and Object Lock; control plane and Aurora only reachable from proxies; workers have egress only; admin ports (/healthz, /readyz, metrics) not public; containers non-root with no secrets in env or logs; dev-only settings (insecure_dev, anonymous Grafana, open Trino, test-hooks) cannot reach a release.

Run cargo deny --locked check and cargo audit; confirm Actions are pinned to commit SHAs and workflows use minimal permissions:; confirm release images are built without the test-hooks feature.

Review in this order, because each crate’s trust is built on the one before. For each: unwrap/expect/indexing on untrusted input, unchecked arithmetic on lengths, unbounded allocation, missing deny_unknown_fields, secrets without Secret<T>, authorization done after an effect, and constant-time comparison of secrets.

  1. limen-core, limen-auth, limen-ca, limen-policy: OIDC and bot login, identity parsing, certificate issuance, policy evaluation.
  2. limen-session, limen-tunnel, limen-proto: grants, tunnel, wire schema.
  3. limen-control-plane: login, enrollment, console, API authorization.
  4. limen-proxy: listener, routing, header handling, peering.
  5. limen-worker, limen-kube, limen-component: protocol modules (largest attack surface).
  6. limen-audit: spool, chain, manifests.
  7. limen-cli: local listeners, credential storage, detached connections.
  • One Bug per finding (severity, boundary, reproduction, suggested fix).
  • Update threat-model.md §7 (review log) and §6 (residual risks) for anything accepted rather than fixed.
  • Add a regression test for every fixed finding, in limen-e2e or the crate’s tests, so it runs in CI.
  • Done when every row in section 3 has a recorded result (pass, finding, or not applicable with a reason) and every crate in section 4 has been reviewed. Record both in the issue.