Security & compliance

Fail-closed by default

Every control below points at the code that implements it.

Where this page comes from Spooktacular's security controls are curated by hand in a shipped inventory (SecurityControlInventory.swift) and rendered verbatim by spook security-controls. Each control names its implementation file and, where one exists, the test that proves its behavior. Everything on this page is drawn from that inventory and the deployment guides that ship in the repo. Last updated: July 15, 2026.

Host-to-control-plane mTLS

The spook serve control plane speaks TLS with a TLS 1.3 floor enforced in code, server-side and client-side, and hot-reloading the certificate preserves that floor. Setting SPOOKTACULAR_TLS_CA_PATH enables mutual TLS: the server presents its certificate and a client certificate is required on every request. This maps to NIST SP 800-52 Rev 2 and OWASP ASVS V9.1.

An operator can prove the floor is live: a -tls1_2 handshake to port 8484 fails, and a -tls1_3 handshake with a client cert returns Verify return code: 0 (ok).

proving the TLS 1.3 floor
$ openssl s_client -connect <host>:8484 -tls1_2
…the handshake fails: the floor rejects TLS 1.2 outright.

$ openssl s_client -connect <host>:8484 -tls1_3 -cert client.pem -key client.key

Verify return code: 0 (ok)

Every HTTP response, regardless of status: carries OWASP ASVS V14.4 security headers (X-Content-Type-Options, CSP default-src 'none', HSTS, X-Frame-Options, Referrer-Policy, Cache-Control).

Credentials in the Keychain

Secrets are held in the macOS Keychain, never exported to disk in production. API bearer tokens, TLS private keys, and GitHub PATs are stored with kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly (OWASP ASVS V6.4, Apple Keychain Services). The API bearer token is read from the Keychain when available; an environment variable is only a fallback, and spook doctor --strict flags that fallback as a warning.

Signing keys go further: they are SEP-only, with no software-key fallback. Private key bytes are generated inside the Secure Enclave and never leave it, so they remain non-extractable even under full kernel compromise. The daemon resolves keys by Keychain label and fails at startup if the label is missing. Silent ephemeral keys are not possible.

Per-action authentication (ASVS MFA)

Administrative actions require a live human, not a standing session. This satisfies OWASP ASVS controls without relying on an "operator workstation is already MFA'd" assumption.

  • Admin CLI gate: RBAC assign/revoke and other admin commands go through a LocalAuthentication presence gate (LAContext.deviceOwnerAuthentication), mapping to OWASP ASVS V4.3.1. It fails closed on headless hosts unless SPOOKTACULAR_ADMIN_PRESENCE_BYPASS=1 is set, and every bypass is logged to OSLog at .error.
  • Break-glass signing: hardware-bound signing in the Secure Enclave, with each signing operation gated by Touch ID, Apple Watch, or passcode. This targets OWASP ASVS V2.7.1, NIST SP 800-63B AAL3, and FIPS 140-3 Level 2 for the SEP.
  • Signed operator-to-API requests: each request is signed with P-256 ECDSA and verified with a nonce replay cache, a ±60s clock-skew window, and canonical-string body-hash binding, no shared static tokens. The trust allowlist is SPOOKTACULAR_API_PUBLIC_KEYS_DIR. Maps to OWASP ASVS V2.10 and OWASP API Top 10 2023 A02.

Data at rest

VM bundles under ~/.spooktacular/vms/<uuid>.vm/ hold disk images that on a developer laptop routinely contain source code, signing material, and injected tokens. On portable Macs (detected via IOKit battery presence), bundle files are written with the CompleteUntilFirstUserAuthentication (CUFUA) file-protection class, applied on create and inherited across clone and snapshot writes. This maps to OWASP ASVS V6.1.1, V6.4.1, and V14.2.6.

  • CUFUA closes a specific gap FileVault alone does not: a powered-off laptop with a compromised FileVault recovery key. The per-file key is derived from the user passcode at first unlock, so bundles stay encrypted until the user authenticates after boot.
  • It is not applied on desktops, Mac minis, or EC2 Mac hosts, where headless LaunchDaemons must boot before any user login. Precedence is env var (SPOOKTACULAR_BUNDLE_PROTECTION) > GUI Settings → Security > auto-detect.
  • spook doctor --strict reports every bundle's protection class, and BundleProtection.verifyInheritance plus a five-case test suite assert zero inheritance violations across create, clone, and snapshot paths.
  • Provisioning scripts (runner registration tokens, remote-desktop credentials) are staged mode 0700 and deleted in a defer block on every exit path of spook create, shrinking the on-disk window from host lifetime to the duration of the command.
FileVault is the enforcement layer FileProtectionType is a no-op without FileVault; when FileVault is off, spook doctor warns that the declaration is present but unenforced.

Ephemeral credentials

VMs never ship long-lived cloud access keys. An operator binds a VM to a cloud IAM role; the host mints short-lived ES256 JWTs from the SEP-bound workload-token issuer, and the VM exchanges them for temporary credentials via standard OIDC federation (AWS STS AssumeRoleWithWebIdentity). This maps to OpenID Connect Core 1.0 and OWASP ASVS V2.10 ("no unchanging credentials"). The signing key is SEP-bound, and the most common ES256 pitfall (DER vs. raw signature encoding) is pinned by a test.

GitHub runner registration tokens carry a 1-hour single-use TTL, so exfiltration after the VM consumes the provisioning script yields a burned token.

Multi-tenant isolation & warm-pool reuse

In multi-tenant deployments, every request carries a TenantID, and scheduler gates ensure one tenant cannot schedule onto another tenant's host pools: the control that prevents cross-tenant reuse and warm-pool leakage (MultiTenantIsolation.canReuse, mapping to SOC 2 CC6.1).

  • Tenant quotas: active VMs are counted per tenant at create and clone time; exceeding the quota returns a 403 with a denial reason.
  • Break-glass is disabled by default in multi-tenant mode. Opening a shell requires both a tenant with breakGlassAllowed set and a role holding break-glass:invoke. Neither is sufficient alone.

Authorization (RBAC)

Access is deny-by-default RBAC with a runtime mutation API (OWASP ASVS V4.1, NIST SP 800-162). Assignments are persisted atomically to ~/.spooktacular/rbac.json and survive restarts. An unassigned actor calling /v1/vms receives a 403 with the missing permission named (vm:list).

Audit & non-repudiation

The audit trail is layered: OSLog, a JSONL file, and a kernel-enforced append-only backing file. The append-only file sets the BSD UF_APPEND flag, verified on init, so the kernel blocks overwrites even from root without an explicit chflags nouappnd first (NIST SP 800-53 AU-9).

The daemon fails closed A production preflight refuses to start the daemon without an audit sink in any tenancy mode, and without RBAC plus mTLS in multi-tenant mode.

Code signing & notarization

Release builds use Apple's Hardened Runtime and an RFC 3161 code-signing timestamp (codesign --options runtime --timestamp); codesign -d --verbose=4 shows flags=0x10000(runtime), a TeamIdentifier, and a Signed Time. Ad-hoc builds skip the timestamp because Apple's TSA won't sign unsigned objects.

inspecting a release build
$ codesign -d --verbose=4 Spooktacular.app

flags=0x10000(runtime)

TeamIdentifier=

Signed Time=
  • CI signing follows fastlane's recommended pattern: match stores certificates and provisioning profiles in a separate private git repo encrypted with MATCH_PASSWORD, App Store Connect API-key auth avoids interactive 2FA, and setup_ci provisions an ephemeral keychain and forces match into readonly mode so CI can never mint or revoke certs.
  • Zero third-party Swift dependencies: Package.swift has no .package(url:) entries, verified by CI, so every security primitive comes from Apple SDKs (CryptoKit, Security, Network).

Verify it yourself

None of this asks for trust. Spooktacular is open source, and the control inventory is designed for spot-checking: run spook security-controls to print every control with its implementation and test file, and spook doctor --strict to probe the live deployment (it exits non-zero if any required control is missing).

$spook security-controls
$spook doctor --strict

The full source is at github.com/Spooky-Labs/spooktacular, and the deployment guides (DEPLOYMENT_HARDENING.md, DATA_AT_REST.md, SIGNING.md) ship in the docs/ directory.

An index of each control and the standards it names:

ControlMaps to
Host-to-control-plane mTLSNIST SP 800-52 Rev 2; OWASP ASVS V9.1; OWASP ASVS V14.4 (response headers)
Credentials in the KeychainOWASP ASVS V6.4; Apple Keychain Services
Per-action authenticationOWASP ASVS V4.3.1, V2.7.1, V2.10; NIST SP 800-63B AAL3; FIPS 140-3 Level 2; OWASP API Top 10 2023 A02
Data at restOWASP ASVS V6.1.1, V6.4.1, V14.2.6
Ephemeral credentialsOpenID Connect Core 1.0; OWASP ASVS V2.10
Multi-tenant isolation & warm-pool reuseSOC 2 CC6.1
Authorization (RBAC)OWASP ASVS V4.1; NIST SP 800-162
Audit & non-repudiationNIST SP 800-53 AU-9
Code signing & notarizationRFC 3161 (code-signing timestamp)