> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sengol.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Platform operator

> Install the appliance, load the licence, connect identity, and hand out access.

You run the appliance. Everyone else depends on you for a working install,
a valid licence, sign-in, and the right roles.

**1. Install**

Docker Compose, Helm, or Terraform, on your own infrastructure.

**2. Load the licence**

One signed file, checked once at start-up.

**3. Sign in as the first admin**

Bootstrap secret or bootstrap admin user.

**4. Connect identity**

Local users, Entra ID / OIDC, SAML, or SCIM.

**5. Issue API tokens**

`sk_` tokens for developers and CI, in `local` auth mode.

**6. Publish the evidence key**

Give auditors the public key they verify against.

**7. Set who approves intakes**

How many approvers a new agent needs, and whether one person may approve their own.

## 1. Install

Every install needs PostgreSQL. It holds the signed records and all
configuration. The API image also serves the Console at `/console` on port
`8080`.

> [tab] Docker Compose
>
> For a single host. The compose file runs `postgres` and `sengol-api`.
>
> ```bash theme={null}
> git clone https://github.com/sengol-io/sengol.git && cd sengol
> make keys                                   # Ed25519 evidence-signing key into .env
> docker compose up -d postgres
> docker compose run --rm --entrypoint alembic sengol-api upgrade head
> docker compose up -d
> curl -s http://localhost:8080/health
> ```
>
> The compose defaults are development secrets. Override every
> `SENGOL_*` secret and `POSTGRES_PASSWORD` in `.env` before the host is shared.
> The compose file sets `SENGOL_ALLOW_OWNER_RUNTIME_ROLE=1`, so the API runs as
> the role that owns the tables. That is for development only.

> [tab] Helm
>
> For Kubernetes 1.26+ with Helm 3.14+.
>
> ```bash theme={null}
> kubectl create namespace sengol
> kubectl create secret generic sengol-license \
>   --namespace sengol --from-file=license=sengol.lic
>
> helm upgrade --install sengol oci://ghcr.io/sengol-io/charts/sengol \
>   --namespace sengol \
>   --set license.existingSecret=sengol-license \
>   --set secrets.SENGOL_AUDIT_URI="postgresql+asyncpg://user:pass@host:5432/sengol" \
>   --set secrets.SENGOL_SIGNING_KEY="$(openssl rand -hex 32)" \
>   --set secrets.SENGOL_JWT_SIGNING_KEY="$(openssl rand -hex 32)"
> ```
>
> `values-aws.yaml` and `values-openshift.yaml` ship as presets.
>
> The API refuses to start when its database role owns the guard version
> tables. Run migrations as an owner role (`SENGOL_MIGRATION_AUDIT_URI`) and
> put a separate runtime role in `SENGOL_AUDIT_URI` (`SENGOL_RUNTIME_DB_ROLE`).
> The chart does not set `SENGOL_ALLOW_OWNER_RUNTIME_ROLE`.
>
> The chart pulls `ghcr.io/sengol-io/sengol-platform`. If you mirror images,
> mirror that repository. `image.tag` is empty by default, which uses the
> chart's `appVersion`, the matching release.
>
> To scrape `GET /metrics` with the Prometheus Operator, set a token and turn
> on the `ServiceMonitor`. The chart does not create one by default.
>
> ```bash theme={null}
> helm upgrade --install sengol oci://ghcr.io/sengol-io/charts/sengol \
>   --namespace sengol --reuse-values \
>   --set secrets.SENGOL_METRICS_TOKEN="$(openssl rand -hex 32)" \
>   --set metrics.serviceMonitor.enabled=true
> ```
>
> The `ServiceMonitor` presents `SENGOL_METRICS_TOKEN` as a bearer token and
> scrapes every `metrics.serviceMonitor.interval` (default `30s`). Add
> `metrics.serviceMonitor.additionalLabels` if your Prometheus selects
> monitors by label, for example `release: kube-prometheus-stack`. With no
> token, `/metrics` answers `503` and serves nothing.

> [tab] Terraform
>
> Modules for AWS, Azure and GCP live in `terraform/` in the repository.
> The AWS stack also provisions a KMS key and its IAM grant. Inputs are
> in each stack's `variables.tf`.

### Settings every install needs

| Variable | Purpose |
| - | - |
| `SENGOL_AUDIT_URI` | PostgreSQL DSN, `postgresql+asyncpg://...` |
| `SENGOL_TENANT_ID` | The install's tenant label. Signed into every record. Set it once. |
| `SENGOL_SIGNING_KEY` | HMAC key that signs records |
| `SENGOL_JWT_SIGNING_KEY` | Signs session and API tokens. The API refuses to start without it. |
| `SENGOL_EVIDENCE_SIGNING_KEY` | Ed25519 private key (PEM) for countersignatures. `make keys` writes one. |
| `SENGOL_LICENSE_FILE` or `SENGOL_LICENSE` | The licence file path, or the token inline |

> [!WARNING]
>
> Changing `SENGOL_TENANT_ID` after records exist hides those records from
> list and report reads. Pick the value before the first record is written.

Today every shipped install path signs with the local backend: the HMAC key
lives in the process environment. Treat `SENGOL_SIGNING_KEY` as you would a
database master key.

## 2. Load the licence

The licence is a signed file, verified offline when the API process starts.
There are no per-feature checks and no calls home.

### Get a licence

You do not need a licence to try Sengol on a laptop: the
[quickstart](https://docs.sengol.io/quickstart.md) runs on the built-in `dev` licence. A real install
needs a licence file from Sengol.

1. Request a pilot at [sengol.io/signup](https://sengol.io/signup.html) with
   your name, work email and company, and choose **Run it yourself**. Personal
   webmail addresses are not accepted, and a pilot is one per company domain.
2. Sengol follows up by email. A pilot is arranged with Sengol after the
   request; it is not self-serve. If the form cannot reach Sengol it opens an
   email draft to [trial@sengol.io](mailto:trial@sengol.io). Send it to complete the request.
3. You receive a licence file. A pilot licence runs 14 days with the full
   capability set, nothing gated or metered, and trial licences get no grace
   period.
4. Put the file in place as below, then start the appliance. A renewal takes
   effect on the next start, so restart after replacing the file.

Where the file goes depends on the install path:

| Install | Licence input |
| - | - |
| Docker Compose | Set `SENGOL_LICENSE` (the token itself) in `.env`. Compose does not mount a file or forward `SENGOL_LICENSE_FILE`, and without `SENGOL_LICENSE` it starts on the `dev` licence. |
| Helm | Store the file in a Secret and set `license.existingSecret`, as in step 1. The chart mounts it at `/etc/sengol/license/sengol.lic`. |
| Terraform (AWS) | Set `license_secret_arn` to a Secrets Manager secret holding the token. The Azure and GCP stacks have no licence input. |

Check what a process will see:

```bash theme={null}
SENGOL_LICENSE_FILE=/etc/sengol/sengol.lic sengol license status
```

The local stack uses the literal `dev` licence, which is fine to inspect:

```bash theme={null}
SENGOL_LICENSE=dev sengol license status
```

```text theme={null}
State:      dev
Tier:       dev
...
Licensed:   yes
Mode:       full

This is the `dev` literal, not a real license. Never use it for a deployment of record.
```

| State | What happens |
| - | - |
| `valid` / `expiring_soon` | Full service. The Console warns 30 days before expiry. |
| `expired_grace` | Keeps running for 14 days after expiry (trial licences get no grace). |
| `expired` | A **restart** boots evidence-only mode: read, verify and export still work; everything else returns `402`. A running process is never stopped. |
| `absent` / `invalid` | The API refuses to start. |

## 3. Sign in as the first admin

Use one of two bootstrap paths:

* **Bootstrap admin user** (local sign-in): set `SENGOL_BOOTSTRAP_ADMIN_EMAIL`
  and, optionally, `SENGOL_BOOTSTRAP_ADMIN_PASSWORD`. If you leave the password
  out, one is generated and printed once in the API log. The user must change it
  at first sign-in.
* **Bootstrap secret**: set `SENGOL_CONSOLE_ADMIN_SECRET` and exchange it for a
  one-hour admin token. This works whatever the identity mode, so it is also
  your way in if the IdP is down.

```bash theme={null}
ADMIN_JWT=$(curl -s -X POST "$SENGOL_API_URL/v1/auth/token" \
  -H "Content-Type: application/json" \
  -d "{\"grant_type\":\"console_admin\",\"secret\":\"$SENGOL_CONSOLE_ADMIN_SECRET\"}" \
  | python -c 'import sys, json; print(json.load(sys.stdin)["access_token"])')
```

Paste the token into the Console's **Sign in with an API token instead**
field. Open **Settings → General** to confirm the licence, the signing keys,
and the active ControlBook.

![Settings General tab showing licence, ControlBook and policy catalog](https://mintcdn.com/sengol/WAzD6pTFOjHwDiMz/images/console-settings-general.png?fit=max\&auto=format\&n=WAzD6pTFOjHwDiMz\&q=85\&s=fb259b1082df47edf9c04ecc2b744152)

Settings → General: licence tier, active ControlBook, policy catalog.

## 4. Connect identity

The sign-in mode is chosen once, at start-up: **OIDC** if
`SENGOL_OIDC_PROVIDER_URL` is set, else **SAML** if `SENGOL_SAML_*` is set,
else **local**.

### Local users

Create a user with a role:

```bash theme={null}
curl -s -X POST "$SENGOL_API_URL/v1/users" \
  -H "Authorization: Bearer $ADMIN_JWT" -H "Content-Type: application/json" \
  -d '{"email":"alex.chen@yourbank.example","display_name":"Alex Chen","role":"developer","password":"Change-me-on-first-login-1"}' \
  | tee user.json
```

```json theme={null}
{"id":"01a10cab-...","org_id":"dev-tenant","email":"alex.chen@yourbank.example","role":"developer",...}
```

You can do the same in **Settings → Access → Identity → Users**:

![Settings Access tab with the create-user form and user list](https://mintcdn.com/sengol/WAzD6pTFOjHwDiMz/images/console-settings-access.png?fit=max\&auto=format\&n=WAzD6pTFOjHwDiMz\&q=85\&s=5a8b5b1cc4bbf64ec0967080bbabfe57)

Settings → Access: create users, assign roles, see break-glass status.

* A password is optional. Without one, the user can still get an `sk_` token.
* TOTP is available but not enforced. Each user enrols from their own account.
* You cannot grant a role with permissions you do not hold yourself.

### Entra ID or another OIDC provider

Register an application in your IdP, define app roles such as
`sengol.admin` and `sengol.auditor`, then set:

```bash theme={null}
SENGOL_OIDC_PROVIDER_URL=https://login.microsoftonline.com/<tenant-id>/v2.0
SENGOL_OIDC_ISSUER=https://login.microsoftonline.com/<tenant-id>/v2.0
SENGOL_OIDC_CLIENT_ID=<application-id>
SENGOL_OIDC_CLIENT_SECRET=<client-secret>
SENGOL_OIDC_REDIRECT_URI=https://sengol.yourbank.example/v1/sso/callback
SENGOL_OIDC_CLAIM_SOURCE=app_role   # default is oidc_groups
```

Map each IdP role or group to a Sengol role. The highest `priority` wins
when a user matches several:

```bash theme={null}
curl -s -X POST "$SENGOL_API_URL/v1/sso/role-mappings" \
  -H "Authorization: Bearer $ADMIN_JWT" -H "Content-Type: application/json" \
  -d '{"external_key":"sengol.auditor","role_id":"auditor","priority":10}'
```

You can also manage mappings in **Settings → Access → Providers → SSO**. A
user who matches no mapping signs in as `viewer` (change it with
`SENGOL_OIDC_DEFAULT_ROLE`).

> [!NOTE]
>
> The Docker Compose file does not forward `SENGOL_OIDC_*` variables. Add
> them to the `sengol-api` service's `environment` block.

### SCIM provisioning

Mint a SCIM bearer token and paste the `token` value into your IdP's
provisioning settings. It is shown once and works only on `/scim/v2/*`:

```bash theme={null}
curl -s -X POST "$SENGOL_API_URL/v1/scim/token" \
  -H "Authorization: Bearer $ADMIN_JWT" \
  | python -c 'import sys, json; print(json.load(sys.stdin)["token"])'
```

Point the IdP at `https://<your-host>/scim/v2`.

### Which role for whom

| Person | Role |
| - | - |
| Platform admin | `admin` |
| Identity team | `access-admin` (manages users only; sees no evidence) |
| Agent developer | `developer` |
| Human reviewer | `member` or `developer` (deciding needs `audit:write`) |
| Risk or compliance lead | `cro` |
| Second approver, four-eyes sign-off | `approver` |
| Independent model validator | `validator` |
| Internal or external auditor | `auditor` |
| Read-only stakeholder | `viewer` |

Every permission is listed in [Access governance](https://docs.sengol.io/guides/access-governance.md).

## 5. Issue API tokens

`sk_` tokens exist only in `local` auth mode. Minting one on an install that
signs people in through an IdP returns `503`; see
[Credentials by journey](https://docs.sengol.io/guides/credentials.md).

Developers and CI jobs use an `sk_` token. Mint one for a user. It is shown
once and expires after 30 days by default:

```bash theme={null}
USER_ID=$(python -c 'import json; print(json.load(open("user.json"))["id"])')
curl -s -X POST "$SENGOL_API_URL/v1/users/$USER_ID/tokens" \
  -H "Authorization: Bearer $ADMIN_JWT" -H "Content-Type: application/json" \
  -d '{"description":"laptop"}'
```

```json theme={null}
{"token_id":"01a10cab-...","token":"sk_dev-tenant__Fp3Y...","created_at":"...","expires_at":"..."}
```

The token carries the user's role. In GitHub Actions, prefer OIDC to a
stored token; see [Gate releases in CI](https://docs.sengol.io/guides/ci-gate.md#4-run-it-in-github-actions).

## 6. Publish the evidence key

Auditors verify countersignatures against your appliance's Ed25519 public
key. They should get it from you, not from the evidence pack. The key list
is public:

```bash theme={null}
curl -s "$SENGOL_API_URL/v1/audit/verification-keys" | python -m json.tool
```

```json theme={null}
{
  "hmac_keys": [],
  "keys": {
    "c5d2e5892a3fd29e": {
      "algorithm": "Ed25519",
      "public_key": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----\n",
      "status": "active"
    }
  }
}
```

Publish it somewhere your auditors already trust, such as your internal
policy repository, and record its key id.

## 7. Set who approves intakes

A new agent is registered when an approver approves its intake. Out of the
box one person is enough, and that person may be the one who submitted it.
Approving needs an interactive sign-in: a password, SSO, or `sengol login --device` approved in the Console. The bootstrap-secret token in step 3
cannot approve, so use one of those.

Set the rule per risk tier. `required_approvals` counts the person who
approves. `same_actor_rule` is `allow` (the default), `reason` or `block`.
You need `sod:override`, which `admin`, `approver` and `cro` hold.

```bash theme={null}
curl -sf -X PUT "$SENGOL_API_URL/v1/sod/approval-policy/HIGH" \
  -H "Authorization: Bearer $ADMIN_JWT" -H "Content-Type: application/json" \
  -d '{"required_approvals": 2, "same_actor_rule": "block"}'
```

Now a `HIGH` intake needs two different approvers, and the submitter cannot
be one. Read the settings back:

```bash theme={null}
curl -sf "$SENGOL_API_URL/v1/sod/approval-policy" -H "Authorization: Bearer $ADMIN_JWT"
```

Remove the row to return a tier to the default:

```bash theme={null}
curl -sf -X DELETE "$SENGOL_API_URL/v1/sod/approval-policy/HIGH" -H "Authorization: Bearer $ADMIN_JWT"
```

The Console's **Settings → Access → SoD** tab sets the approver count. The
same-person rule is set through the API.

To send intakes to specific teams, such as Privacy, set up a
[review workflow](https://docs.sengol.io/guides/review-workflows.md).

## Day-two checks

* `GET /health` returns `{"status":"ok",...}`.
* The Console's **Settings → System Health** tab shows component status.
* Back up PostgreSQL. It holds every record and every setting.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.