Security & compliance
Fail-closed by default
Every control below points at the code that implements it.
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).
$ 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 unlessSPOOKTACULAR_ADMIN_PRESENCE_BYPASS=1is 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 --strictreports every bundle's protection class, andBundleProtection.verifyInheritanceplus 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
0700and deleted in adeferblock on every exit path ofspook create, shrinking the on-disk window from host lifetime to the duration of the command.
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
403with a denial reason. - Break-glass is disabled by default in multi-tenant
mode. Opening a shell requires both a tenant with
breakGlassAllowedset and a role holdingbreak-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).
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.
$ codesign -d --verbose=4 Spooktacular.app … flags=0x10000(runtime) … TeamIdentifier=… … Signed Time=…
- CI signing follows fastlane's recommended pattern:
matchstores certificates and provisioning profiles in a separate private git repo encrypted withMATCH_PASSWORD, App Store Connect API-key auth avoids interactive 2FA, andsetup_ciprovisions an ephemeral keychain and forcesmatchintoreadonlymode so CI can never mint or revoke certs. - Zero third-party Swift dependencies:
Package.swifthas 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:
| Control | Maps to |
|---|---|
| Host-to-control-plane mTLS | NIST SP 800-52 Rev 2; OWASP ASVS V9.1; OWASP ASVS V14.4 (response headers) |
| Credentials in the Keychain | OWASP ASVS V6.4; Apple Keychain Services |
| Per-action authentication | OWASP 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 rest | OWASP ASVS V6.1.1, V6.4.1, V14.2.6 |
| Ephemeral credentials | OpenID Connect Core 1.0; OWASP ASVS V2.10 |
| Multi-tenant isolation & warm-pool reuse | SOC 2 CC6.1 |
| Authorization (RBAC) | OWASP ASVS V4.1; NIST SP 800-162 |
| Audit & non-repudiation | NIST SP 800-53 AU-9 |
| Code signing & notarization | RFC 3161 (code-signing timestamp) |