Reference

Security overview

What runs where, what leaves your network and what the vendor holds, identity and access, request handling, audit, secrets, licensing and supply chain, for a procurement or security review.

SectionReference
UpdatedNovember 9, 2026
EditionSelf-hosted Enterprise, public beta

Applies to self-hosted ShakerScan Enterprise 0.2.0-beta.51 (public beta, November 2026). Each statement below was checked against this release's code on the date above. Items marked not yet are not claimed.

What runs where

Everything runs inside the customer's environment on a Docker host the customer operates.

ComponentSourceNetwork positionData it holds
Public engine (api, worker, ui, postgres, redis)Open-source ShakerScan images, digest-pinned, AGPL-3.0Private Compose networks; no published portsTargets, credentials for authorized targets, findings, evidence, HTTP transactions
Enterprise gatewayPrivate image from the vendor registry, digest-pinned and signedThe only service reachable from the customer network, behind Caddy TLSIdentities, sessions, service tokens, audit metadata, MFA secrets (sealed), licensed-slot ledger
CaddyPublic image, digest-pinnedPorts 80/443TLS certificates

All engine requests go through the gateway. Workers cannot reach the API directly; the API and workers use separate database-facing networks; the storage initializer runs with no network. No container mounts the Docker socket. The gateway runs read-only as an unprivileged user with all capabilities dropped.

What scans may reach: public addresses and, by default from 0.2.0-beta.48, private ones too (10/8, 192.168/16, 100.64/10 and unique-local IPv6: an intranet application or a device on the customer's network). An administrator turns private addresses off with --private-network-targets refuse at install or PRIVATE_NETWORK_TARGETS=refuse upgrade.sh; the engine then refuses them outside a Lab environment. Whatever that setting and the environment say, this deployment's own addresses are never scanned by a web Scan, a Hunt or a network scan (engine 2.11.3 or later; network scans on every engine): 172.16.0.0/12, where Docker places the deployment's containers by default, loopback, link-local and cloud metadata. A host whose Docker address pools were changed is fenced only by loopback, link-local and metadata.

Data that leaves the environment

No customer data is sent anywhere by the Enterprise gateway: no telemetry, no licence activation or check-in, no usage reporting. Apart from sign-in calls to the identity provider the customer configures and calls to the Jira site an administrator connects (below), the running gateway makes two requests of its own, both read-only and both only for an administrator's browser session in the console (the routes refuse service tokens and other roles):

  • The public engine release catalog from GitHub: the latest main commit of andriyze/shakerscan, install/STABLE_VERSION at that commit and the published release list. Read when an administrator's console loads, each time the administrator switches to the Overview, every five seconds while an engine update is requested or running, and afresh, bypassing the cache, when an administrator requests an engine update.
  • The vendor's signed gateway release channel from shakerscan.com (https://shakerscan.com/enterprise/gateway-channel.jwt). Read when an administrator's console loads, each time the administrator switches to the Overview, every five seconds while a gateway update is requested or running, and afresh when an administrator requests a gateway update.

A successful answer is kept for ten minutes and reused; a failed one is not kept, so the next read tries again. Both are plain HTTPS GET requests with no credentials, cookies, licence or installation identifier: the server sees the host's address and the generic client name python-httpx. The channel request goes through the outbound proxy when one is configured and follows no redirects; the catalog requests go directly. The gateway accepts the channel only with the vendor's signature and lists only the releases it names (each pinned by digest in ghcr.io/andriyze/shakerscan-enterprise) that are newer than the running gateway. Nothing is pushed and nothing installs itself: an engine update starts only when an administrator requests it in the console, and a gateway update only when an administrator requests it and confirms with a fresh sign-in or passkey (step-up). Without that egress the gateway runs normally; set SHAKERSCAN_ENTERPRISE_GATEWAY_CHANNEL_FILE to a channel file the vendor sends you, or upgrade the gateway with upgrade.sh (install guide).

The host updater, started every 30 seconds by its systemd timer, only asks the local gateway whether an update was requested; it contacts nothing outside the host until an administrator has requested one. Applying an engine update, it takes a backup, reads that release's publication record (certification receipt and image lock) from GitHub through the gateway's command line, downloads the release's installer from raw.githubusercontent.com at the exact source commit, runs it (the installer downloads the release's files from GitHub) and pulls the engine images from Docker Hub. Applying a gateway update, it verifies the image's signature and SBOM attestation with cosign and pulls the image from ghcr.io.

The public engine's own outbound behaviour (target traffic, optional AI providers configured by the customer, model endpoints) is the customer's configuration and network policy. The generated deployment disables automatic retesting, attack-surface discovery defaults, AI verification of findings and the AI operations router (autonomous Hunt) unless the customer turns them on.

Jira, when an administrator connects it (Jira page of the console): the gateway calls that one site only, with the token the administrator entered, which it checks against Jira before saving and stores encrypted with the installation key. A ticket carries the finding's title, severity, CVSS, CWE, location without its query string, the scanner's description and a link back to the finding; never evidence, requests, responses, notes or cookies. When a retest proves a fix, the gateway adds a comment and moves the ticket to a done status; it reads the tickets' status back every 15 minutes unless an administrator turns that off.

Hunt runs in each operator's own terminal agent, connected with a service token. That agent sends what the gateway returns to it (target addresses, requests and responses, findings) to the model provider the operator configured for the agent. ShakerScan does not see or control that provider; choose one your data policy allows.

The complete outbound list for a generated deployment, for an egress allowlist:

DestinationComponentWhen
registry-1.docker.io, auth.docker.io, production.cloudflare.docker.com (Docker Hub)host Dockerpulling the pinned public engine images at install, at upgrade and when the host updater applies an engine update
ghcr.io, pkg-containers.githubusercontent.comhost Docker, cosign on the hostpulling the Enterprise gateway image with the customer's read token at install, at upgrade and when the host updater applies an update; cosign reads the image's signature and SBOM attestation there at install and before the host updater applies a gateway update
api.github.com, raw.githubusercontent.com (repository andriyze/shakerscan)gatewayread-only fetch of the public engine release catalog, only for an administrator's console as described above: when it loads, on each switch to the Overview, every five seconds during an engine update, and afresh on an engine update request; a successful answer is reused for ten minutes
api.github.com, raw.githubusercontent.com, github.com, release-assets.githubusercontent.com (repository andriyze/shakerscan)gateway command line (run by install.sh and the host updater), the public engine installerread-only fetches at install (the release catalog, to choose the stable release) and, at install and when the host updater applies an engine update an administrator requested, that release's publication record, its installer at the exact source commit and the release's files
shakerscan.com (/enterprise/gateway-channel.jwt)gateway, through the outbound proxy when one is configuredread-only fetch of the vendor's signed gateway release channel, only for an administrator's console as described above: when it loads, on each switch to the Overview, every five seconds during a gateway update, and afresh on a gateway update request; a successful answer is reused for ten minutes, a failed one is not kept
install.shakerscan.comhostthe public engine installer, only on the manual install, upgrade and restore paths
api.github.com and the Sigstore public-good services (Rekor transparency log, TUF root)cosign and gh on the hostverifying the gateway signature, SBOM attestation and engine build attestations at install (install.sh), and the gateway signature and SBOM attestation (cosign only) before the host updater applies a gateway update
acme-v02.api.letsencrypt.org and the ACME HTTP challenge inbound on port 80Caddyonly when no customer certificate was given to prepare
the Jira site an administrator connects (https://your-company.atlassian.net, or your Data Center address)gateway, through the outbound proxy when one is configuredonly when Jira is connected: creating tickets, posting retest-proved fixes, reading ticket status
the OIDC issuer's discovery, authorization, token, JWKS and userinfo endpointsgatewaysign-in, when OIDC is configured (SHAKERSCAN_ENTERPRISE_OIDC_ENDPOINT_HOSTS pins them)
the public certificate-transparency logs (Google's CT log list and the logs it names)engine CT monitor (gungnir-worker)while the CT monitor option is on (the default); reads the logs only, never the targets
the scan targets the customer authorizesengine workersscanning; the workers pin each target's resolved addresses for the run
AI provider or model endpoints the customer configuresengineonly if configured; off in the generated deployment
a private interactsh server the customer configuresengineonly if configured; the tools run with -no-interactsh otherwise

The scanner tools run with update checks disabled; templates and vulnerability databases are baked into the pinned images, so nothing is fetched at scan time. Proxy environment variables are ignored by design (trust_env=False) by the engine's HTTP clients and by the gateway's clients for the engine, the identity provider and the gateway release channel, so no proxy can ever sit between the gateway and the engine. A corporate proxy is configured explicitly instead (prepare --outbound-proxy): Caddy's ACME client, the gateway's calls to the identity provider and its gateway release channel read go through it, as do its calls to Jira; the gateway reads the engine release catalog directly (the rendered deployment gives its container no proxy variables), so behind a proxy-only egress the console says it cannot read the catalog and engine updates are done on the host; the engine's target traffic does not go through it and must be allowed directly at the firewall; image pulls follow the Docker daemon's proxy configuration.

What the vendor holds

The vendor never receives findings, evidence, target credentials, people's identities, audit logs, model keys or Hunt sessions, and has no remote access to your installation. It holds your company name, contacts, installation id and licence terms. A support bundle reaches the vendor only when an administrator downloads it and sends it; it carries versions, health, settings and recent operational records, with secrets, findings, targets and people's names left out.

Identity and access

  • Single sign-on: OpenID Connect authorization code with PKCE, one-use state bound to the browser, nonce check, signed ID-token validation (RS256, ES256), exact issuer match, HTTPS-only provider endpoints restricted to reviewed hosts. Verified against Keycloak and Okta; an Entra ID preset is provided but not yet verified against a live tenant (Okta guide, Entra ID guide).
  • Single sign-on required (optional): an administrator signed in through the provider (or as the break-glass administrator) can require single sign-on for browser sign-in, terminal approvals and pre-authorizations. One named local administrator, the break-glass account, keeps a local way in for when the provider fails: it must hold a passkey or an authenticator app (a security key alone is refused; a passkey is recommended, and the console warns without one), and every such sign-in is audited as a break-glass sign-in. Turning the requirement on ends the sessions of all other local accounts and withdraws open invitations (audited); lifting it later does not bring them back. Service tokens are unaffected. While it is on, five failed attempts for a username within fifteen minutes lock that username's password sign-in: every answer is then the same "too many attempts", even for the right password, unless the same request also carries a valid authenticator code or recovery code (the console's sign-in form sends password and code together once the code field is shown). Passkey sign-in is not affected, so a break-glass administrator with a passkey always gets in; one whose only second factor is a security key waits out the fifteen minutes or asks for rates-reset. While single sign-on is required, the break-glass account is the only local account that can approve from a terminal (single sign-on people approve through the browser link above). Local sign-in can also be turned off entirely. A change that would sign out the administrator making it is refused. Host recovery: rates-reset --username <name> clears a name's sign-in counter at once; settings-reset --key sso_enforced removes the console value after a gateway restart (a value set in the host's environment file still applies).
  • Re-authentication (step-up): decisions that make Hunt-written text guidance, dismiss findings, reveal or share discovered secrets and similar need the person's fresh proof for that one decision: a passkey or authenticator code for local accounts; for single sign-on accounts, a fresh sign-in at the provider (prompt=login, max_age=0), accepted only if the ID token's auth_time is after the request began, it names the same person, it returns to the same browser session, and (when configured) its acr is one the deployment requires. A provider that does not report auth_time cannot step anyone up. Single sign-on people approve terminal requests the same way, in the browser: shakerscan approve --method sso prints a link on this gateway where they review the exact decision and sign in again, and only a service token whose Acts for names that person can start one. As with any re-authentication, a person who is tricked into re-authenticating for a page they did not mean to act on (a page already compromised in their browser) approves that one decision; the console names the decision before asking, and the grant covers only that decision, once, for two minutes. Revealing, granting or promoting a discovered secret on a target's page of the scanner asks in a dialog on that page (the gateway's own script); the same request is then sent once more and the scanner shows its own answer. The gateway never stores the value, writes it to the audit log or puts it in an address, and every reveal is audited with the person, the target and the secret's reference. A finding's status or verdict, the Findings list's bulk change and Accept risk are stepped up in the same dialog on the scanner page.
  • Administrator changes that open a lasting way in need the administrator's fresh step-up for that exact request: adding an administrator or a person with a password, inviting a person (the link lets whoever holds it choose a password), making someone an administrator, setting another person's password or issuing a reset code, issuing a service token, and turning a sign-in protection down (require_mfa, the passkey policy or the security-key switch). The console opens a Confirm it's you dialog that names the change; a passkey, an authenticator code or a fresh single sign-on confirms it (an administrator with no second factor uses their password), and the change is then sent once more. The grant covers that one request, once, for two minutes; Cancel or a wrong answer changes nothing, and a password typed into the change is never kept in the grant or the audit. Disabling someone, raising a protection and every other setting need no step-up. The host command line (cli, root on the host) is not gated.
  • Security-key switch: with Require a security-key touch for person decisions on, every browser step-up above (secrets, triage, administrator changes) is only a touch of a USB or NFC security key registered to the person; codes, passwords and platform passkeys are refused. Add a key under My account before turning it on: turning it off again is itself an administrator change that then needs that touch (on the host, cli settings-reset --key person_decision_proof removes the console value after a gateway restart).
  • Admission: an identity is admitted by its exact subject or by a deliberate first-sign-in group-to-role mapping. Local role changes are authoritative; removing an IdP group does not remove local access (disable the identity in the console). No SCIM (not yet).
  • Roles: viewer (read findings, evidence, scans, targets), operator (start scans, manage credentials and collections), admin (identities, tokens, audit, license). Enforced per request by method and path allowlists; unreviewed engine routes are denied. Everyone can read what each role may do under My account; the table is computed from the same rules.
  • Local accounts: Argon2id password hashing, minimum 14 characters, throttled sign-in, authenticator-app two-step verification with per-code replay protection, and passkeys (WebAuthn, user verification required, bound to the gateway's hostname, signature counters checked, single-use server-side challenges). A passkey satisfies the two-step policy on its own; an account with a passkey and no authenticator app cannot sign in with its password alone. Policy can require two-step verification for administrators or all local users; a required-but-unenrolled session can reach only the enrollment screen. A lost passkey or authenticator is replaced at sign-in by the password and one of ten single-use recovery codes (stored as digests); without codes, recovery is the server-side operator command reset-mfa. Accounts can be created by one-time invitation links, so the person chooses their own password.
  • Service tokens: issued to service identities, shown once, stored hashed, viewer or operator scope, at most 90 days, revocable individually, never able to administer or open the UI.
  • Decisions only a person should make, from a terminal: raising a running Hunt's budget, authorizing a target, changing the environment a target or device is judged in, sharing a credential, writing scope or approval receipts, widening a target's Hunt permissions, confirming a device SSH plan or a device service probe, changing a connected device's address, lockout or policy, and reactivating a discovered secret or changing its expiry. An agent and its person's terminal present the same token, so the gateway cannot tell them apart by the token.
    • In short. Off by default: a token may make these decisions, so an agent using a person's token can too (an accepted risk), and each one is audited. To require the person, an administrator turns on Settings, Scanning, "Require a security-key touch for person decisions from terminals" (with a YubiKey per person; each tries a touch first with shakerscan approve --test, which decides nothing), and each person registers their key once under My account. Each such decision from a terminal then takes one touch of that key in the person's own terminal (shakerscan client 0.11.2 or later). In a browser, only a discovered secret's reveal, grant or promote. Turn the switch off before rolling the gateway back to a release before 0.2.0-beta.47.
    • Off (the default). The request reaches the engine as the client sent it, as on 0.2.0-beta.45 and beta.46, audited as person_decision.token, outcome unproven (method, route, decision, token, the person it acts for; never the body), with a line in shakerscan approve --watch; so is the target authorization the gateway records when a token's active scan, Hunt start, retest or network scan reaches a target nobody authorized.
    • Terminal approvals. In either state a key registered to the person answers a terminal approval (shakerscan approve, --watch, agent --allow) with one touch, no PIN. That holds for a single sign-on person's key registered under My account too, beside the browser sign-in, so it answers without the identity provider's own step-up; it works until an administrator disables the person here, and a disable at the identity provider reaches the gateway at the person's next sign-in. With the switch off the code or the browser sign-in stays the fallback and an approver session still decides by keypress. A knowledge decision or a secret reveal, grant and promote from the terminal is proved with the switch off exactly as on beta.46 (the code, a key with its PIN, or for single sign-on the browser sign-in only).
    • On. A token's person decision is refused (person_decision_required) unless it carries a one-time grant from a touch of a key registered to the person the token acts for: 120 seconds, used once, bound to the method, path, body digest, token and person, looked up by its digest. The touch is user presence (origin the gateway's public URL, relying party its host name; a touch inside another site's frame is refused) by a key whose browser reported it as USB or NFC, or reported no transport; one reported only as built in or a phone never counts. Every terminal approval is one touch too, a deny as well. A device SSH plan's confirmation and an authorization investigation's approval are made in the browser only. The gateway writes who decided into the request and audits its own engine writes. The watch feed takes an operator token and reads at most 15 minutes of audit rows. Decisions tokens made while it was off stay in force: review the person_decision.token rows. Details: Security-key touch: reference.
    • A safety budget per agent session. With the switch on, the Hunts one agent session starts share one safety budget, which its launch touch (shakerscan agent --allow) approved: one balanced Hunt's state-changing requests, ports, hosts, out-of-band interactions and device fragility, raised as its budget.raise bounds raise one Hunt. A running Hunt holds what it may still spend, an ended one what it spent, so cancelling and starting again resets nothing; a start past what is left is the person's decision, and a raise past it is the Hunt's own request. A Hunt counts a ceiling only when its policy (or a capability: start bound) gives it that authority, as the engine resolves its budget, so a read-only Hunt counts nothing. A token without an agent session has one balanced Hunt's ceilings per 12 hours. It bounds those ceilings, not a Hunt's requests, calls or duration.
    • What a touch proves. A person was present at the key: presence, not identity, and not what they saw. It stops an agent that uses the person's token, not a hostile program running as the same operating-system user, which can rewrite the client or start its own request the moment the person expects to touch; a key has no display, so touch only for a prompt you started. The client asks only at a terminal, and asks for a touch the switch requires only when the command's output is the terminal too; an agent in a full pseudo-terminal not started with shakerscan agent can still bring up the prompt. Running the agent as another user, or in a container or VM, with its own connection, is the hardening. Physical access to a plugged-in key is a presence proof, not an identity proof.
  • Sessions: opaque server-side sessions, __Host- secure cookies, one-hour absolute and fifteen-minute idle limits by default, bound to the password credential verified at sign-in. Role, password or factor changes revoke sessions. No provider back-channel logout (not yet).

Request handling

  • Only reviewed request headers are forwarded; cookies, bearer tokens and forwarded-host headers never cross the gateway boundary. Upstream cookies are dropped.
  • Browser mutations require a same-origin Origin header; Enterprise administration also requires a CSRF token. Engine mutations must be JSON.
  • Evidence and other engine responses are served with a sandboxing Content-Security-Policy and an attachment disposition for non-JSON media, so downloaded evidence cannot execute as a page.
  • Streaming responses re-check the session or service token every two seconds (SHAKERSCAN_ENTERPRISE_STREAM_REAUTH_SECONDS, 1-60, default 2), also while a slow client is not reading, and the gateway stops sending within about that interval of a revocation. A download then ends as an error with an engine.stream audit row. An event stream (live scan progress) ends without a row, as its client reconnects or signs in again: cleanly while its client is reading, cut as an error while it is not. A re-check that cannot run (the gateway's state database unavailable) stops the stream too. Data it has already handed to the network is not recalled: the host's socket buffers and the reverse proxy's buffers in front of the gateway, which together can hold megabytes, can still deliver it to the client after the revocation. A revocation therefore bounds when the gateway stops reading from the engine for that response, not the last byte the client receives; every new request with the revoked session or token is refused at once. A response that the engine ends early or breaks off mid-body is aborted, not ended cleanly.
  • Engine API requests are rate-limited per person and per service token (heavy reads more tightly; 429 with Retry-After), and long-running work (Hunt actions, streams, exports and downloads) can hold only part of the gateway's engine connections, so it cannot starve ordinary requests; a share of that part is kept for Hunt actions, and one person or token holds at most four by default.
  • Request and response sizes and upstream timeouts are bounded.
  • Active work needs a target authorization on the engine. The gateway records it on the submitter's behalf, once per target, naming the operator or administrator; the audit shows who authorized which target and when. The engine's own gate still decides, and the gateway never fabricates a receipt id.
  • Scanning with a target's saved credentials needs a credential-tier approval for that target, which an operator's New Scan creates. An operator may create only this kind: a scope of the target's own address and host (no other hosts, root domains or redirects) in the production or lab environment (any other, staging included, is an administrator's), previewed by the same account in the last 15 minutes, and an approval of tier active or credential with no action binding, expiring within 7 days and 15 minutes. Deletion and dangerous-tier approvals are an administrator's. Revoking a target's authorization does not cancel approvals already issued; they keep working until they expire, at most 7 days and 15 minutes later.

Audit

Administrative actions, sign-in outcomes, engine requests and responses (method and path, never bodies, query strings, tokens or passwords) are recorded with actor and timestamp, and the client address of the request that caused them (none for host jobs and the command line; behind your own proxy or tunnel, see --trusted-front-proxies in the installation guide), viewable in the console and exportable as NDJSON. The export is the complete log, streamed, or a time range: in the console's Audit log view, choose From and To dates (UTC; leave both empty for everything), the order, and whether to leave out the per-request scanner records (engine.read, one per read, and engine.request / engine.response, two per change, and engine.capture, one per uploaded batch of a browser capture; most of the log on a busy host; left out by default in the console). Opening and closing a browser capture are named rows (capture.create, capture.close) with the person, the channel (browser or service token), the target and the collection. Through the API an administrator calls GET /_enterprise/audit/export with optional from and to (Unix seconds or ISO 8601; from inclusive, to exclusive, and a bare date as to covers that whole day), order=newest|oldest (default newest) and proxy=include|exclude (default include). Each export is an audit.export row naming the administrator and the filters; an administrator can start ten exports per five minutes, and a further one is refused (HTTP 429 with Retry-After) without a row. GET /_enterprise/audit/export/allowance answers whether one may start now and how many seconds to wait if not, without taking from the limit; the console asks it before starting the download and tells the administrator how long to wait. The scanner UI's static files (/_next/static/..., icons) and health polls are not audit rows, and a Command Arsenal dry run (POST /arsenal/execute with execute=false) of one of the reviewed reads (the commands a service token may run, Hunt's read-only tools among them) is one engine.read row naming the command. Any other dry run may write (the engine runs every read-only command in process, and some of them write records, scope.preview and campaign.create among them), so it is recorded and kept as a change. Failed sign-ins and failed redemptions (a wrong invitation, password-reset or connect code) by someone not signed in are recorded one by one, with their address, while two allowances hold: ten per address for each kind of failure (then one every 30 seconds) and thirty per kind for the installation (then one every two minutes). Each of the ten kinds has its own allowance. Beyond that they are counted, one login.failures row a minute at most with the count per kind, the five busiest addresses and the five busiest refused subjects. So someone who is not signed in adds at most about 8,900 rows (about 2.3 MB, measured) a day, from any number of addresses, and a flood of any kinds at once (the single sign-on callback can be called by anyone) never hides the rows of another kind. A refusal after the identity provider authenticated someone (an identity no role is given to) is recorded with the subject and the groups it carried: the first three for each subject, then one every ten minutes, within that kind's allowance, so that a provider that authenticates outsiders (a public one, a realm with self-registration) cannot add rows without limit. The audit store is on the host and is not tamper-proof against a host administrator (remote streaming not yet). Retention, by default: per-request scanner reads (engine.read, engine.stream, engine.capture) are removed after 30 days, counted first into daily totals per action; security events (sign-ins, refusals, step-ups, the failed redemptions above) and changes made on the engine (each change's engine.request and engine.response) are kept for 400 days, a twelve-month audit period and a month, and then written to the audit archive before they are removed: files on the gateway's state volume, one per month in the export's NDJSON, each part recorded in the audit log with its SHA-256 (cli audit verify checks them), carried by every backup and put back by a restore, whose check compares the archive with the log and says what is missing. The archive is kept until an administrator removes months of it under the customer's own policy (cli audit archive-remove --before YYYY-MM, which records each file and its SHA-256 in the log first and refuses a file retention may still write). Every other record (people, roles, settings, tokens, approvals, decisions, updates) is never removed. Each period is set in enterprise.env (at least 90 days, or 0 to keep forever). A retention run that cannot write the archive (a full volume, a file it may not write) removes nothing it could not archive, records why (audit.retention, outcome incomplete), shows it on the health card (Audit retention) and is tried again six hours later.

Secrets and keys

SecretWhereProtection
PasswordsGateway state volumeArgon2id hashes only
Sessions, service tokensGateway state volumeSHA-256 digests only
Authenticator secretsGateway state volumeAES-GCM sealed with a 32-byte key kept on the read-only secrets mount, outside the state volume
OIDC client secret, CA bundle, licenseCustomer secrets mount, read-onlyFile permissions for the gateway user only
Target credentials, evidencePublic engine (PostgreSQL, results directory)The public engine's own controls

Identity backups are AES-GCM encrypted under a passphrase-derived key (scrypt) and include the MFA sealing key. The audit archive's backup beside each one is encrypted the same way, under the same passphrase, and holds the archive's files only (no key). Neither has a size limit, and neither is staged outside the gateway's state volume. Restores are create-only, revoke every session and token, and withdraw open invitations, password-reset codes, connect links and approvals.

Licensing

Offline. A license is an Ed25519-signed token bound to the installation id and verified against the vendor key embedded in the package. It limits concurrent work: the gateway reserves a slot before each scan, finding retest, network scan and on-demand ASM batch, and refuses submissions beyond the allowance. The gateway records a new licence in the audit log (license.change, with the old and new licence id, edition, slots and expiry) the first time it sees it after the file is replaced. When a licence expires (after any grace days it grants), the gateway refuses every change to engine data except cancelling running work: no new scans, retests, triage or credential changes. Sign-in, administration, reading existing results and cancellation continue. A customer-attested usage summary can be exported for renewal. No network licensing, no phone-home.

Supply chain

Enterprise images are built by a GitHub Actions workflow from an annotated release tag, which the vendor creates only on a commit that passed its local release checks. The workflow installs its tools from hash-pinned lock files, runs the complete test suite, builds and start-tests the exact image, refuses it if it has a high or critical vulnerability with a fix available, pushes it to a private registry, signs the digest with Sigstore keyless signing and attaches an SPDX SBOM attestation. Every action the workflow uses is pinned to a commit. The image installs its Python libraries from a hash-pinned lock. Engine images are pulled by digest from the public release lock file; at install they are checked against the public repository's GitHub build attestations.

Verify the gateway image before you run install.sh or any docker run. install.sh checks signatures itself (step 4 of 8), but only after it has run the image to extract the delivery kit and look up the engine release, and has installed the public engine runtime. After docker login ghcr.io with your pull token, and with the digest you were sent:

IMAGE=ghcr.io/andriyze/shakerscan-enterprise@sha256:<digest>
cosign verify \
  --certificate-identity-regexp '^https://github.com/andriyze/shakerscan-saas/.github/workflows/publish-enterprise.yml@refs/tags/enterprise-v[0-9A-Za-z.+-]+$' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  "$IMAGE"
cosign verify-attestation --type spdxjson \
  --certificate-identity-regexp '^https://github.com/andriyze/shakerscan-saas/.github/workflows/publish-enterprise.yml@refs/tags/enterprise-v[0-9A-Za-z.+-]+$' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  "$IMAGE" > /dev/null

Both must succeed. The identity accepts only the vendor's publish workflow running from a release tag; a manual or branch build cannot carry it.

Operations

Create-only deployment generation, an upgrade command that keeps data and refuses downgrades, versioned state with pre-migration snapshots, and a scripted backup (daily, encrypted, reported on the console's health card) and restore covering the engine database, evidence, encrypted identity metadata and deployment files; a full restore drill of this release onto a clean host (not yet). Engine updates requested in the console take a backup first and roll back on failure; gateway updates check the image signature before pulling it and roll back on failure. Single-process gateway; high availability (not yet). Kubernetes packaging (not yet). An ECS Fargate evaluation layout exists and was exercised; it is not the supported production posture.

Known limitations to state in a questionnaire

One organization per installation, no project-level isolation between teams; no SCIM or SAML; no provider logout; audit is local only; HA and Kubernetes are not delivered; a single Linux Compose host is the supported target. Recurring scans run from the console from gateway 0.2.0-beta.48 (Recurring scans); on earlier ones a host timer submits scheduled scans with a service token (Scheduled scans). A scheduled run never authorizes its target; it runs only under a standing authorization a person recorded.

On the vendor side: no reproducible builds; no air-gapped installation bundle; no third-party penetration test yet; no two-person release control (one vendor account publishes and signs releases).