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).