Security
Landing page for Abysslink's security posture. For reporting a vulnerability, see SECURITY.md (private disclosure, 90-day coordinated window, CVE coordination available).
- Full threat model: security/threat-model.md
- OS hardening guide: security/hardening.md
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 verifyorcosign 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 --applyrefuses when Tailnet Lock is not enabled on the live tailnet — the gate readstailscale lock status, not config, sotailnet.lock.enabled=falsecannot bypass it. The only bypass is the explicit--accept-lock-disabledflag (default off, per-invocation). - Overriding the lock gate is audited. Using
--accept-lock-disabledrecords 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 statuscannot 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 doctorreportssec-disk-encryptionas 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 Onprefix, so a disk mid-encryption is never misreported as encrypted, and unrecognizedfdesetup statusoutput 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, andsec-remote-loginare WARN by default and become FATAL underabysslink 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/subtlecompare, 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: keychainselector is stored inabysslink.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 doctorreportssec-duressas 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).