Skip to content

Configuration

Configuration illustration

The SCF Controls Platform runs in two ways, and configuration differs between them:

  • Managed SaaS — a fully-managed application. Configuration is handled through the platform’s web interface; there are no environment files or server settings to manage. Most of this page covers these in-app settings.
  • Self-hosted (open source) — you run the Docker Compose stack and configure it through a .env file that scripts/install.sh writes for you. See Self-hosted environment variables below, and Deployment for the full install.

Self-hosted deployments are configured through a .env file in the checkout. On first run scripts/install.sh writes it for you, and it holds non-secret settings only. Every credential the stack needs is generated by the installer into the secrets directory (SCF_SECRETS_DIR, one 0600 file per credential) and reaches each service as a file. You type no credential into .env on the bundled path. See Credentials and secrets for what lands where, and never commit .env.

Every credential also accepts a NAME_FILE variant (for example DB_PASSWORD_FILE) naming a file that holds the value. The platform resolves each credential in the order database tier, then NAME_FILE, then the plain environment variable. The plain variable in .env is the legacy fallback: it keeps installs that predate the installer working, but it is not the first-run path.

These are the credentials the stack cannot boot without. On a fresh install the installer generates every one of them into SCF_SECRETS_DIR; you never choose, see or type a value.

VariableWritten by the installerPurpose
DB_PASSWORDYes — generated for the bundled Postgres, or collected once for an external databasePostgres password; the backend builds its connection string from the DB_* settings plus this value. Legacy path: set it in .env
API_KEYYesMaster backend API key. Legacy path: openssl rand -hex 32 into .env
SCF_SECRET_KEYYesEncrypts the integration credentials stored in the database. scripts/upgrade.sh generates it if absent and never overwrites it. Back it up
DOWNLOAD_TOKEN_SECRETYesSigns evidence download links; falls back to API_KEY when unset
VITE_API_KEYNoCopy of the API key baked into the frontend bundle at build time, so the browser can call the backend on the API-key sign-in path. Anyone who loads the page can read it, so it is not a secret. Installer-provisioned installs do not need it: the secrets overlay exposes $SCF_SECRETS_DIR to the frontend build, which reads API_KEY from it for the build step only (ignored when VITE_OIDC_ENABLED or VITE_GOOGLE_AUTH_ENABLED is true); rotating the key and running docker compose up -d --build frontend picks up the new value. Plain .env installs set it to the same value as API_KEY and rebuild the frontend after changing it (docker compose up -d --build frontend)
VariableDefaultPurpose
SCF_SECRETS_DIR(written by the installer)Absolute host path of the credentials directory. docker-compose.secrets.yml, scripts/backup.sh and scripts/upgrade.sh all read it. Absent on a legacy .env install
SCF_APP_GID1001 (written by the installer)Linux only. The gid the containers reach host files through: the installer group-owns SCF_SECRETS_DIR (group read) and webclient/public/data (group write) to it, and the compose files hand it to group_add for the services that run as container root with cap_drop: ALL. 1001 is the gid of the backend image’s apiuser. Changing it means re-running scripts/install.sh — editing this line alone leaves SCF_SECRETS_DIR on the old gid. The next scripts/upgrade.sh run does pick the new value up for webclient/public/data, which it re-checks and re-groups on every upgrade, but it never touches the secrets directory. Ignored on macOS, where Docker Desktop remaps bind-mount ownership
COMPOSE_FILEdocker-compose.yml:docker-compose.secrets.yml (written by the installer)Compose file set. The secrets overlay is opt-in: on a legacy install the variable is absent and the base file runs alone. Setting it disables auto-discovery of docker-compose.override.yml, so add that file to the list if you use one
ENVIRONMENTproductiondevelopment | staging | production. Only use development on a trusted local machine. Outside development and test the backend refuses to start on a missing or placeholder credential
OSS_SINGLE_TENANT1Lets the master key admin a single org; fail-closed — disabled at startup if >1 org/member exists
LOG_LEVELinfodebug | info | warning | error
CATALOG_VERSION2025.4SCF catalogue version surfaced on /version; match your imported workbook
PLATFORM_VERSION0.40.0Fallback application version label; .env.example ships it aligned with webclient/package.json. Normally unused — the backend reads the real version from the mounted package.json, falling back to this only if that mount is missing (and to 0.0.0 if unset)
MAX_UPLOAD_SIZE64mLargest request body the frontend nginx proxies to the backend (client_max_body_size). Clears the backend’s own 50 MB cap on the SCF catalogue workbook. You should not need to change it — but see the note below if you run your own reverse proxy

The installer asks whether you want the bundled Postgres container or an external database (scripts/install.sh, or the web installer’s Database step). It probes an external database before it writes anything — DNS, TCP connect, authentication, TLS negotiation, server version, database existence, and a real CREATE TABLE / DROP TABLE privilege check — and refuses to continue if any of those fail. The values it accepts, whether entered as discrete fields or as a connection string, are written to .env as the variables below; the wizard’s screens are shown on First-run setup.

VariableDefaultPurpose
DB_HOSTpostgresDatabase host. postgres is the bundled container; set it to your own host for an external database
DB_PORT5432Database port
DB_NAMEcg_scfDatabase name. It must already exist — the platform does not create it
DB_USERcgDatabase role. It needs CREATE/DROP on the database: Alembic migrations run DDL on every upgrade
DB_SSLMODE(unset)disable | allow | prefer | require | verify-ca | verify-full. Use require or stronger for any database reached over a network you do not control
DB_PASSWORD(generated / collected by the installer)The role’s password. File-backed — see Required above
DATABASE_URL(unset)A complete SQLAlchemy DSN, used verbatim when set and non-empty, overriding every DB_* variable above. The installer leaves it empty

The backend composes its connection string from the DB_* variables whenever DATABASE_URL is empty, URL-escaping the user and password for you. Prefer the components: they are what the installer writes, what the credential file protects, and what keeps the password out of docker inspect.

Authentication (Google OAuth — optional)

Section titled “Authentication (Google OAuth — optional)”

Leave GOOGLE_AUTH_ENABLED=false to run with API-key auth only.

VariablePurpose
GOOGLE_AUTH_ENABLED / VITE_GOOGLE_AUTH_ENABLEDEnable Google sign-in (backend + frontend must match)
GOOGLE_CLIENT_ID / VITE_GOOGLE_CLIENT_IDOAuth client ID (build-time copy baked into the frontend bundle)

Identity provider (OIDC / bundled Keycloak — optional)

Section titled “Identity provider (OIDC / bundled Keycloak — optional)”

Leave VITE_OIDC_ENABLED=false to keep Google / API-key auth. Set it to true to authenticate via OIDC single sign-on — either the bundled Keycloak (docker compose --profile idp up -d) or your own provider. The KC_* variables apply only when the bundled idp profile is running. See Identity Provider for the full setup, the ISSUER-vs-DISCOVERY footgun, and troubleshooting. The localhost defaults below only work when your browser runs on the Docker host itself — on a remote host, set OIDC_ISSUER, OIDC_REDIRECT_URI, and KC_HOSTNAME to the host’s reachable address (see Deploying on a remote host).

VariableDefaultPurpose
VITE_OIDC_ENABLEDfalseFrontend: use redirect-based OIDC sign-in instead of Google. Build-time (baked into the frontend image)
OIDC_ISSUERhttp://localhost:8081/realms/scfPUBLIC issuer; must byte-match the token iss claim and be browser-reachable
OIDC_DISCOVERY_URLhttp://keycloak:8080/realms/scfINTERNAL realm base the backend fetches (bare base, no /.well-known suffix)
OIDC_CLIENT_IDscf-platformOIDC client ID
OIDC_CLIENT_SECRET(generated for bundled Keycloak, or collected for an external provider, by the installer into a 0600 file)Confidential client secret; idp-init writes it into Keycloak at boot. Legacy path: replace the changeme-generate-a-real-oidc-secret placeholder in .env
OIDC_REDIRECT_URIhttp://localhost:5173/api/auth/callbackOAuth callback URI (must be registered on the client)
OIDC_SCOPESopenid email profileRequested scopes
KC_ADMIN_USERadminKeycloak admin user — REQUIRED with --profile idp. Also read by the backend service to provision invited users; see Provisioning invited users
KC_ADMIN_PASSWORD(generated by the installer into a 0600 file; never shown)Keycloak admin password — REQUIRED with --profile idp, and read by the backend service for the same reason. Legacy path: replace the changeme-keycloak-admin placeholder in .env
KC_ADMIN_PASSWORD_FILE(set by the secrets overlay to /run/secrets/KC_ADMIN_PASSWORD)Path to a file holding the password. Takes precedence over KC_ADMIN_PASSWORD; you do not set this by hand
KC_HOSTNAMEhttp://localhost:<KEYCLOAK_PORT>Public base URL Keycloak advertises; default follows a remapped KEYCLOAK_PORT. Set explicitly for remote hosts or a reverse proxy
BOOTSTRAP_ADMIN_EMAIL(blank)If set, idp-init seeds this platform admin with a one-time random temp password printed to the idp-init logs

KC_ADMIN_USER and KC_ADMIN_PASSWORD were only ever read by idp-init, which uses them once to build the realm. The backend service now reads them too. When both resolve and an OIDC realm is configured (OIDC_DISCOVERY_URL or OIDC_ISSUER), inviting a user also creates that user in the bundled Keycloak with a one-time temporary password, which is returned in the invite API response and included in the invitation email when Resend is enabled. Keycloak requires the invitee to replace it at first sign-in.

The backend resolves the password the same way it resolves every other credential: the path in KC_ADMIN_PASSWORD_FILE first, then the environment variable. It is never read from the database — KC_ADMIN_PASSWORD is closed to the integration-secrets table by construction, so a row stored under that name cannot become the credential that controls your realm. It is not exposed in any API response other than the temporary password itself, and never written to the frontend bundle.

Outbound email is off until a Resend API key is present. The key is a tier-3 integration credential: add it under Settings, Integrations (see Integrations). Setting RESEND_API_KEY in .env is the legacy fallback and still works.

VariablePurpose
RESEND_API_KEYResend API key for outbound notification email — legacy .env fallback; prefer Settings, Integrations
RESEND_FROM_EMAILFrom address on notification emails
APP_URLPublic frontend URL used in email links

The backend runs daily Celery beat jobs that generate evidence collection tasks, send due/overdue task notifications, and refresh stale evidence windows. Both flags default to on; set to false (or 0) to opt out.

VariableDefaultPurpose
TASK_AUTOMATION_ENABLEDtrueDaily evidence-task generation plus due and overdue notification jobs
WINDOW_ASSESSMENT_NIGHTLY_ENABLEDtrueNightly sweep that refreshes stale evidence window assessments. Uploads and webhook deliveries also trigger an assessment directly; see WINDOW_ASSESSMENT_ON_INGEST below

The window assessor scores every file collected inside a frequency-derived window as one portfolio. These knobs shape what it sends to the model and when it runs.

VariableDefaultPurpose
ARTIFACT_TYPE_LAZY_EXTRACTIONtrueA fresh install ships the catalog with no required artifact types per control. When this is on, the first window assessment that needs a control’s artifact types extracts them once (one ARTIFACT_TYPE_AI_MODEL call per control) and caches the result on the catalog row. Controls that were attempted are not re-tried, and an extraction failure never blocks the assessment. Set to false to use only what the backfill CLI (backend/scripts/extract_artifact_types.py) has populated
WINDOW_ASSESSMENT_TEXT_BUDGET150000Character budget for the evidence text in one window prompt. Identical collector payloads are deduplicated before the budget is applied, and anything dropped is disclosed in the assessment’s coverage findings
WINDOW_ASSESSMENT_ON_INGESTtrueA browser upload or webhook delivery schedules the window assessment for that evidence item instead of waiting for the nightly sweep. Only tracked evidence items (those with a collection frequency) are scheduled. Set to false to rely on the sweep alone
WINDOW_ASSESSMENT_INGEST_DEBOUNCE_SECONDS120How long the assessor waits after an ingest before running, so deliveries arriving inside the interval collapse into one assessment that sees all of them. The debounce is held in Redis; if Redis is unavailable the assessment is still scheduled

Four flags decide which assessment layer the platform treats as primary. All default on: the windowed, record-level AI assessment is the primary assessment surface and the per-file assessor is the diagnostic layer beneath it (see the AI evidence assessment guide). Set the backend flags on both the backend and celery-worker services; the two answer the same questions and a flag set on only one produces two different scores for the same organisation.

VariableDefaultPurpose
ENABLE_PER_WINDOW_REVIEWtruePer-window review: window verdicts fill the Awaiting confirmation queue, the window review panel appears on the evidence page and the Frequency Health tile on the dashboard. With it on, the per-file document review endpoint answers 410 Gone (with a Sunset header and a pointer to the window review endpoint) for any evidence item that already has a window assessment; evidence with no window assessment yet still accepts per-file review
VITE_ENABLE_PER_WINDOW_REVIEWtrueThe frontend’s build-time twin of the flag above. It is baked into the bundle: change it with docker compose up -d --build frontend, never with a restart. The app compares it against GET /api/features at startup and logs a mismatch to the browser console; a bundle built with it off against a backend with it on leaves reviewers with no working review path
ENABLE_WINDOW_ASSESSMENT_KSItrueThe Evidence Quality axis reads window verdicts in preference to per-file verdicts for evidence that has a window assessment
ENABLE_COMPOSITE_KSItrueThe Evidence Quality axis reads the per-control composite (the roll-up of window verdicts across a control’s evidence) in preference to both. Independent of the window flag: composite on and window off falls back to per-file for controls with no composite

Only the literal value true (any case) turns a backend flag on, and an unset or empty value takes the default. Turning any of them off is an opt-out for a deployment that wants to stay on per-file review; it does not remove the window assessments already stored.

Start here: most installations should not use these variables at all. Evidence storage is configured in the application, under Settings, Evidence storage, where a platform administrator sets a store for the whole installation and an organisation administrator can point their own organisation at a store of its own. Those configurations live in the database, hold their credentials encrypted, and can be changed without restarting anything.

The variables below are the fallback beneath that. When an organisation has no store of its own and the installation has no platform store either, the backend synthesises one from this environment so that installations predating the settings screen keep working untouched. Anything configured in the application wins over anything set here.

The normal path for an installation holding real evidence is a bucket of your own: install with ./scripts/install.sh --no-minio so no object store is bundled at all — the minio and minio-init services never start and the stack runs without them — then configure the store from the application, or set the S3 variables below. See Deployment → Evidence storage.

Left alone, the stack falls back to the bundled MinIO container so a first run needs no cloud account. That store is an evaluation and pilot path only: MinIO archived its community edition in April 2026 and the shipped image carries unpatched advisories with no community fix — see the troubleshooting guide.

VariableDefaultPurpose
COMPOSE_PROFILES(written by the installer)Contains storage when the bundled MinIO is part of this stack. Without it minio and minio-init never start. May also contain idp, as idp,storage
EVIDENCE_STORAGE_BOOTSTRAP(empty)bundled_minio or none, written by the installer. COMPOSE_PROFILES is read on the host and never reaches a container, so this is how the backend learns which object store it has. bundled_minio makes it seed the platform storage configuration on first boot
AWS_ENDPOINT_URLhttp://minio:9000Internal S3 endpoint; blank = use real AWS S3. A --no-minio install is written blank
EVIDENCE_PUBLIC_ENDPOINThttp://localhost:9000Browser-facing endpoint for presigned URLs
EVIDENCE_BUCKETevidenceBucket name. Blank on a --no-minio install, which is what makes the platform report itself as having no evidence storage rather than pointing at an address where nothing is listening
MINIO_ROOT_USER / MINIO_ROOT_PASSWORD(generated by the installer into 0600 files)Bundled MinIO credentials. Legacy path: replace the minioadmin / changeme-… placeholders in .env; the backend refuses to start on them
AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY(generated by the installer — a pair of its own, not the MinIO root credentials)S3 credentials the backend uses. On the bundled path minio-init creates a MinIO user for this pair whose policy names the evidence bucket and nothing else, so a leaked application credential is not the object store’s root account. For real AWS S3, put your own values in the two files (or in .env on the legacy path)
AWS_DEFAULT_REGIONeu-west-1S3 region
AWS_SESSION_TOKEN(unset)Optional third credential, for temporary credentials issued by AWS STS. Read the same file-first way as the key pair. Leave unset for long-lived keys
AZURE_STORAGE_ACCOUNT_NAME / AZURE_STORAGE_ACCOUNT_KEYRetired and ignored. Azure Blob is no longer a storage backend. Setting these selects nothing; the backend logs a warning and carries on with the store it would otherwise have used. See the note below
EVIDENCE_CONTAINERevidenceRetired and ignored, with AZURE_STORAGE_*

The API keys in this table are tier-3 integration credentials, managed encrypted in the database from Settings, Integrations (see Integrations). The .env variable is the legacy fallback, read only when nothing is stored in the database or in a NAME_FILE file.

VariablePurpose
ANTHROPIC_API_KEYEnables AI-assisted evidence / window assessment and AI vendor assessments. Must be set on the Celery worker for vendor assessments; without it the vendor assessment engine runs in mock mode and returns a clearly marked sample report
VENDOR_AI_MODELClaude model for vendor assessments — see AI model selection
VENDOR_AI_MOCKSet to 1 to force mock vendor assessments even when an API key is configured
HIBP_API_KEY, NVD_API_KEYThird-party breach / vulnerability lookups for vendor research
APPLICATIONINSIGHTS_CONNECTION_STRINGTelemetry export
SCF_XLSXPath to your SCF workbook for the catalogue importer (default ./catalog-source/scf.xlsx)

Every AI feature resolves its model through a single registry (backend/services/model_registry.py). No model id is hard-coded at a call site, so repointing a model is an environment change, not a deploy.

One variable moves the platform. Set SCF_AI_MODEL and every compliance role follows it: evidence assessment, artifact-type extraction, vendor assessment, system recipe generation and AI-augmented document generation.

Per-role overrides beat the global. Each role also has its own variable, for holding one service back or pushing one forward (“everything on the new model except doc-gen, which regressed”). Precedence: per-role variable, then SCF_AI_MODEL, then the built-in default.

VariableWhat the feature doesDefault
EVIDENCE_AI_MODELAI evidence assessment: reads an uploaded evidence file and answers each SCF assessment objective of its mapped controls with an advisory designation and rationale; also drives per-window portfolio assessments. A human reviewer confirms or overrides every suggestionclaude-opus-5
ARTIFACT_TYPE_AI_MODELArtifact-type extraction: derives the artifact types (policy, screenshot, export, report) a control expects. Runs lazily from the window assessor on first use (see ARTIFACT_TYPE_LAZY_EXTRACTION) and from the backfill CLIclaude-sonnet-4-6
VENDOR_AI_MODELVendor assessment: the vendor assessment engine researches a third-party vendor with web search and drafts the security / data-protection assessment reportclaude-sonnet-4-6
SYSTEMS_AI_MODELSystem recipe generation: produces the L1–L4 evidence-collection recipes for a system, grounded in the vendor’s real admin console and APIsclaude-sonnet-4-6
DOC_GEN_AI_MODELAI-augmented document generation: writes Tier-2 ISMS documents (domain policies, procedures, standards) from scoped control data. Tier-1 documents are template-rendered and never call a modelclaude-sonnet-4-6

Notes:

  • Defaults are not uniform. Evidence assessment defaults to a stronger (and more expensive) model than the other roles, so setting SCF_AI_MODEL flattens every role onto one id — use a per-role override to keep a role apart.
  • Unregistered ids are honoured, with a warning. An id not declared in the registry is still sent to the provider — production must be able to escape a bad default without a deploy — but its per-assessment cost is recorded as NULL rather than guessed, and the weekly model-liveness check cannot vouch for it.
  • Cost-tracked roles need a priced model. Evidence assessment and artifact-type extraction write a cost figure per run; their registry defaults must declare a price (enforced in CI).
  • Recreate, don’t restart. A model change takes effect on container recreation (docker compose up -d --force-recreate backend celery-worker); docker compose restart reuses the old environment and will not pick it up.

Organisation administrators can configure:

SettingDescriptionAccess
Organisation NameDisplay name for your organisationAdmin only
Primary FrameworkDefault compliance framework for dashboard metricsAdmin only
User RolesAssign roles to team membersAdmin only

Individual users can configure:

SettingDescription
Notification PreferencesEmail notification settings
Display PreferencesUI preferences (if available)

By default a self-hosted install authenticates with the master API_KEY, which the installer generates into the secrets directory (it lives in .env only on a legacy install) — no external identity provider is required. Optional OIDC single sign-on and Google Sign-In can be enabled on top. See Authentication for the full setup of each method.

  1. User clicks “Sign in with Google”
  2. Google authenticates the user
  3. Platform receives user identity (email, name, profile picture)
  4. User is granted access based on their organisation membership

Any Google account type works: Google Workspace, personal Gmail, or Google Cloud Identity.


The platform sends email notifications for:

Notification TypeTrigger
User InvitationsWhen invited to join an organisation
Task AssignmentWhen assigned to a control or task
Due RemindersBefore task due dates
Overdue AlertsWhen tasks pass their due date
@MentionsWhen mentioned in comments

On a self-hosted install, all data lives in your own infrastructure:

  • PostgreSQL (bundled container or your own instance) — the system of record
  • Object storage — evidence files in an S3-compatible store of your own, or the bundled MinIO while you evaluate
  • Backups are your responsibility — see Backup & Restore

The bundled combination is the only one the shipped scripts back up in full. scripts/upgrade.sh (and scripts/backup.sh) dump the bundled Postgres container and tar the bundled MinIO volume. Choose an external database or external object storage and that store falls outside what they capture — silently, and without an error. Back it up yourself before you rely on either script. That is a reason to configure your provider’s own protection when you move off the bundled stores, not a reason to stay on them. See what the scripts do not cover.

You control retention entirely: data persists in your database and object store until you delete it, and you can export or back up at any time.


FeatureDescription
API key / OIDC / Google OAuthAuthentication methods — see Authentication
Role-Based AccessAdmin, Editor, Viewer roles (enforcement coming soon)
Organisation IsolationEach organisation’s data is completely separate
  • HTTPS Only — All connections are encrypted
  • OAuth 2.0 — Industry-standard authentication
  • Session Management — Automatic session timeout for security
  • Audit Logging — All actions are logged for compliance

The platform currently supports:

IntegrationPurpose
Google Sign-InUser authentication
Email (Resend)Notification delivery

Single Sign-On (SSO) via OIDC has shipped — see Identity Provider for bundled Keycloak or bring-your-own-provider setup.

Additional integrations are planned for future releases:

  • SIEM/SOAR integration
  • Ticketing system integration (Jira, ServiceNow)
  • API access for automation

Access under Settings, Backups in the web interface (the Tenant Export / Import card):

FeatureDescription
Download Tenant ExportExport one organisation’s working data as a JSON file
Import Tenant DataUpload a previous export, review the preview, then confirm

Backups include:

  • All scoped controls and their status
  • Evidence tracking configurations
  • Users and organisation settings
  • Assignments, comments, and tasks
  • Notification history

This list describes the in-app JSON export, which is a partial tenant export, not a disaster-recovery backup. It omits:

  • SCF catalog data (this is reference data provided by the platform)
  • System configuration (managed by the platform)
  • Evidence files, the audit log, audit engagements, evidence assessments, generated documents, vendors, teams, custom risk definitions — and 26 other organisation-scoped tables
  • The credentials directory (SCF_SECRETS_DIR) and SCF_SECRET_KEY, without which a restored database cannot decrypt its stored integration credentials

For a backup that covers the whole database, the evidence volume and the credentials together, use scripts/backup.sh — see Backup & Restore.


The platform version is displayed:

  • In the footer of each page
  • From GET /api/version
ComponentDescription
Platform VersionApplication version number
API VersionBackend API version
Catalog VersionSCF Controls Catalog version

ResourcePurpose
This DocumentationSelf-service help and guides
In-App SupportContact support from within the platform
Email Supportsupport@scfcontrolsplatform.com

When reporting issues, include:

  • Browser type and version
  • Steps to reproduce the problem
  • Any error messages shown
  • Screenshots if applicable