Skip to content

Security

Landing page for Abysslink's security posture. For reporting a vulnerability, see SECURITY.md (private disclosure, 90-day coordinated window, CVE coordination available).

Threat model summary

Abysslink defends the path from a phone to a development rig over a hostile network, assuming the phone may be lost or stolen. It does not replace endpoint security on a fully-compromised laptop, and it does not audit the upstream tools (Tailscale, mosh, ntfy) it orchestrates — it configures them safely.

Threat Mitigation
Stolen phone Revocable device keys (Tailnet Lock); abysslink panic revokes immediately.
Stolen laptop Disk encryption enforced (doctor fails closed); ntfy/webui/metrics bind tailnet-only.
Compromised VPN account Tailnet Lock — a rogue node cannot join without your signing key.
Secrets leaked via logs Audit log stores diff hashes only; no secrets on argv; secrets live in the OS keychain; doctor enforces audit-log permissions (sec-audit-log-perms).
Accidental public exposure Funnel rejected at the config schema; listeners never bind 0.0.0.0.
Supply-chain tampering Reproducible builds; cosign signature + SLSA provenance verified on install/upgrade.
Injection via notification ntfy is receive-only on the phone — no command-execution path back to the rig.
Unencrypted disk abysslink doctor exits 2 (fatal).

Run abysslink threat-model to print this table with the live ✓/✗ status of each defense on your machine.

Guarantees

  • No public internet exposure. All listeners bind to the tailnet IP; Tailscale Funnel is rejected at the YAML schema level.
  • No secrets on the command line or in logs. Secrets are read from stdin or the OS keychain; the audit log records titles and diff hashes only.
  • Every mutation is backed up and recorded in a hash-chained, optionally-signed audit log, and is reversible via abysslink backup restore / abysslink uninstall.
  • Dry-run by default. Nothing is mutated without an explicit --apply.
  • Signed, reproducible binaries. Verify with abysslink verify or cosign verify-blob.
  • Derived ntfy topics meet a 128-bit entropy floor. Rig ntfy topics generated by Abysslink carry a ≥128-bit random suffix; a derived topic below the floor is rotated on config write.

Host-posture hardening (v4.1 backlog)

Four setup-guide footguns are enforced by dedicated gate/doctor/HMAC seams:

  • Tailnet Lock is a fail-closed hard gate at apply time. abysslink up --apply refuses when Tailnet Lock is not enabled on the live tailnet — the gate reads tailscale lock status, not config, so tailnet.lock.enabled=false cannot bypass it. The only bypass is the explicit --accept-lock-disabled flag (default off, per-invocation).
  • Overriding the lock gate is audited. Using --accept-lock-disabled records an audited consent entry (op=lock-override-consent, target=tailnet-lock) with no secret and no message body.
  • An undeterminable Tailnet Lock status is non-overridable. If tailscale lock status cannot be determined (probe error, non-zero exit, or unparseable output), the gate fails closed and cannot be overridden even with --accept-lock-disabled.
  • FileVault mid-encryption is treated as not-safe. While FileVault encryption is in progress, abysslink doctor reports sec-disk-encryption as FATAL — mid-encryption is gated exactly like an unencrypted disk.
  • The FileVault status parse fails closed. The in-progress state is matched before the FileVault is On prefix, so a disk mid-encryption is never misreported as encrypted, and unrecognized fdesetup status output is reported as UNKNOWN, never as encrypted.
  • The per-rig notification HMAC is domain-separated. It binds a fixed protocol domain tag, a format version, and the rig identity as length-prefixed fields, so a signature cannot be replayed across rigs or protocols even under a shared key; the comparison is constant-time (hmac.Equal).
  • New host-posture checks are strict under at-risk. sec-tailnet-lock, sec-autologin, and sec-remote-login are WARN by default and become FATAL under abysslink doctor --profile at-risk.

Duress decoy (v4.1, opt-in)

The duress decoy is a human-factors mitigation for casual coercion (a shoulder-glance, a border-guard "show me", a grabbed terminal). It ships OFF; enable it with abysslink duress enable --apply.

  • A decoy credential shows a benign view AND degrades the real session for real. Entering the decoy credential renders a benign rig view (a quiet machine, no fleet, no live sessions) and, at the same time, fires the kill-switch: armed agents are frozen and killed and a persisted lockdown latch is set so nothing re-arms.
  • The decoy is non-destructive by design and by test. There is no code path that deletes, truncates, or overwrites real data; a destructive duress-wipe is an explicit anti-feature (security theater + self-DoS + detectable). The static and behavioural no-wipe guarantee is enforced by internal/duress/nowipe_test.go.
  • The credential comparison is constant-time over fixed-width digests. The presented value is reduced to a 32-byte argon2id digest before a crypto/subtle compare, and every candidate slot is evaluated with no short-circuit, so neither the credential length nor which slot matched leaks through timing.
  • Duress credentials never live in config. Only a non-secret secret_source: keychain selector is stored in abysslink.yaml; the real and decoy credential digests live in the OS keychain.
  • Duress activation is audit-logged without revealing which credential was used. The activation records a single generic hash-only entry (no decoy-vs-real discriminator, no credential body).
  • An enabled-but-inert decoy fails closed in doctor. If duress is enabled but no decoy credential is resolvable, abysslink doctor reports sec-duress as FATAL (a false sense of safety is worse than none).

Explicit non-goals

  • Protecting a laptop whose OS is already compromised (rootkit, malicious local user).
  • Hardening the phone-side terminal app or the phone's OS.
  • Auditing the security of Tailscale / Headscale / NetBird / mosh / ntfy themselves.
  • Anonymity or traffic-shape obfuscation — Abysslink is about access control, not anti-surveillance.
  • Forensic plausible-deniability and destructive duress-wipe. The duress decoy defends only casual coercion (seconds-to-minutes); it does not hide the real config from a forensic adversary, and there is deliberately no destructive duress-wipe.

Supported versions

Only the latest minor release line receives security fixes. Upgrade with abysslink upgrade.

Version Supported
Latest minor release line (v4.x at time of writing)
Older release lines

CI security gates

Every change runs through make lint (golangci-lint incl. gosec + nolintlint), a standalone gosec pass with a single canonical exclude set, and semgrep (p/r2c-security-audit, p/golang). Releases are reproducibility-checked (repro-check workflow) and fuzz-tested (fuzz workflow).