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.
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.
| Component | Source | Network position | Data it holds |
|---|---|---|---|
Public engine (api, worker, ui, postgres, redis) | Open-source ShakerScan images, digest-pinned, AGPL-3.0 | Private Compose networks; no published ports | Targets, credentials for authorized targets, findings, evidence, HTTP transactions |
| Enterprise gateway | Private image from the vendor registry, digest-pinned and signed | The only service reachable from the customer network, behind Caddy TLS | Identities, sessions, service tokens, audit metadata, MFA secrets (sealed), licensed-slot ledger |
| Caddy | Public image, digest-pinned | Ports 80/443 | TLS 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
maincommit ofandriyze/shakerscan,install/STABLE_VERSIONat 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:
| Destination | Component | When |
|---|---|---|
registry-1.docker.io, auth.docker.io, production.cloudflare.docker.com (Docker Hub) | host Docker | pulling the pinned public engine images at install, at upgrade and when the host updater applies an engine update |
ghcr.io, pkg-containers.githubusercontent.com | host Docker, cosign on the host | pulling 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) | gateway | read-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 installer | read-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 configured | read-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.com | host | the 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 host | verifying 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 80 | Caddy | only 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 configured | only when Jira is connected: creating tickets, posting retest-proved fixes, reading ticket status |
| the OIDC issuer's discovery, authorization, token, JWKS and userinfo endpoints | gateway | sign-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 authorizes | engine workers | scanning; the workers pin each target's resolved addresses for the run |
| AI provider or model endpoints the customer configures | engine | only if configured; off in the generated deployment |
| a private interactsh server the customer configures | engine | only 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_enforcedremoves 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'sauth_timeis after the request began, it names the same person, it returns to the same browser session, and (when configured) itsacris one the deployment requires. A provider that does not reportauth_timecannot step anyone up. Single sign-on people approve terminal requests the same way, in the browser:shakerscan approve --method ssoprints 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_proofremoves 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, outcomeunproven(method, route, decision, token, the person it acts for; never the body), with a line inshakerscan 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 theperson_decision.tokenrows. 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 itsbudget.raisebounds 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 acapability: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 agentcan 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.
- 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
- 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
Originheader; 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 anengine.streamaudit 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
| Secret | Where | Protection |
|---|---|---|
| Passwords | Gateway state volume | Argon2id hashes only |
| Sessions, service tokens | Gateway state volume | SHA-256 digests only |
| Authenticator secrets | Gateway state volume | AES-GCM sealed with a 32-byte key kept on the read-only secrets mount, outside the state volume |
| OIDC client secret, CA bundle, license | Customer secrets mount, read-only | File permissions for the gateway user only |
| Target credentials, evidence | Public 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).