Authentication, permissions and audit#
manent identifies who uses it, decides what they can do per application and environment and records every relevant action. The manent client always goes through the API, so the same rules apply to whoever uses the API directly and to whoever uses the CLI.
flowchart LR
U["User (OIDC, manent client)"] -->|Bearer JWT| API
CI["CI pipeline (token, manent client)"] -->|Bearer token| API
OP["Platform operator"] -->|manent-server admin| DB[("Database")]
API --> G["Permissions (RBAC)"]
G --> E["Engine"]
G --> A["Audit"]
E --> W["Worker"]
W --> A
OP -.->|admin.recover, admin.prune| A
Authentication#
auth:
mode: enabled # none = no authentication: development only
oidc:
issuer: https://sso.example.com/realms/corp
audience: manent
username_claim: preferred_username
tokens:
- name: ci-pipeline
sha256: "9f2c…"
groups: [ci]
With mode: enabled every request to /api/v1 must have the Authorization: Bearer <token> header. Without a token, or with an invalid one, the response is 401 with WWW-Authenticate: Bearer. /healthz stays accessible without authentication.
Users: OIDC#
manent verifies JWTs issued by an OIDC identity provider (Keycloak, Microsoft Entra ID, Okta, Google, ...): signature, issuer, audience and expiry. Signing keys are read from the issuer (discovery and JWKS) when serve starts and reloaded automatically when they rotate.
| Key | Default | Description |
|---|---|---|
issuer |
Issuer URL. | |
audience |
Expected value of the aud claim, typically the client ID registered for manent. |
|
username_claim |
Required. Claim with the user name, unique and not changeable by the user: see Choosing the user name claim. A token without it is refused. | |
groups_claim |
groups |
Claim with the groups, used by permissions. |
policy_claims |
[email, name] |
Claims exposed to the permission policy (input.identity.claims); the others never reach it. |
ca_file |
Additional CAs to reach the issuer. | |
ui.client_id, ui.scopes, ui.redirect_path |
Public client (PKCE) of the web interface. |
The client (portal, script, CI with federated identity) obtains the token from the identity provider and passes it to manent. manent handles neither login nor sessions.
Automated systems: static tokens#
Static tokens can be configured for CI pipelines. The configuration holds only the token's sha256, never its value:
TOKEN=$(openssl rand -hex 32) # store it in the CI secret store
printf '%s' "$TOKEN" | sha256sum # write it to auth.tokens[].sha256
The comparison is constant-time. Each token can have an expiry (expires_at, RFC 3339): after that date it is no longer valid.
Token file#
Tokens in auth.tokens change only by restarting serve. To add and revoke them without a restart use a separate file, with the same format:
tokens:
- name: ci-pipeline
sha256: "9f2c…"
groups: [ci]
expires_at: 2027-06-30T00:00:00Z
- Reload. manent checks the file at most every 5 seconds and reloads it when its modification time or size changes. A token removed from the file stops being valid within 5 seconds.
- Invalid file. At startup it is an error. Afterwards, a modified but invalid file is reported in the log and the tokens read last time stay in use: an editing mistake does not block CI.
- Removed file. All its tokens are revoked.
The file can be a mounted Kubernetes Secret or managed by Vault Agent; it contains only hashes, but it must still be write-protected.
Identities#
Every identity has the form <method>:<name>, used in permissions, in the audit log and in the deploy status:
| Method | Example | Origin |
|---|---|---|
oidc |
oidc:alice |
OIDC user (username_claim claim). |
token |
token:ci-pipeline |
Static token. |
cli |
cli:joeg |
Operating system user running manent-server admin (recorded in the recovery and retention audit). |
none |
none:anonymous |
API with auth.mode: none. |
Permissions (RBAC)#
Permissions assign a role to users or groups on a set of applications and environments. With auth.mode: none permissions are disabled and everything is allowed.
rbac:
bindings:
- groups: [team-a] # OIDC or token groups
role: deployer
applications: ["team-a-*"]
environments: [dev, test]
- groups: [team-a]
role: tester # dry-run only in production
applications: ["team-a-*"]
environments: [prod]
- groups: [release-managers]
role: operator
applications: ["*"]
environments: [prod]
- subjects: ["token:ci-pipeline"]
role: deployer
applications: ["*"]
environments: [test]
All fields accept patterns with *. A binding applies if the identity matches one of the subjects or belongs to one of the groups. An action is allowed if at least one binding grants it: there are no deny rules. To decide permissions and denials in Rego, with revisions, approval and simulation, see Permissions as a policy (rbac.engine: opa).
Roles and actions#
| Action | What it allows |
|---|---|
deployment.read |
Status, result and list of deploys. |
deployment.dry-run |
Enqueue a dry-run. |
deployment.submit |
Enqueue a deploy, including one scheduled with schedule.at. |
deployment.cancel |
Cancel a deploy. |
deployment.rollback |
Manual rollback to a previous version. |
deployment.approve |
Approve or reject a deploy awaiting approval. |
deployment.freeze-override |
Bypass a freeze window. |
schedule.read |
Read recurring schedules. |
schedule.write |
Create, update, pause, resume and delete schedules. Creating a schedule also requires deployment.submit. |
platform.read |
See the workers: hosts, versions, regions, heartbeat and running workflows. It is not tied to an application: any binding that grants it is enough. |
freeze.write |
Create and delete freeze windows at runtime, on every application and environment of the window. |
policy.read, policy.write, policy.approve |
Read, push (and evaluate, measure) and activate the revisions of the governance policy. Like platform.read they are not tied to an application. |
| Predefined role | Actions |
|---|---|
viewer |
deployment.read, schedule.read |
tester |
viewer + deployment.dry-run |
approver |
viewer + deployment.approve |
deployer |
tester + deployment.submit, deployment.cancel, deployment.rollback |
operator |
deployer + schedule.write, platform.read |
release-manager |
viewer + freeze.write |
admin |
all, including deployment.freeze-override and the policy.* actions |
deployer and operator cannot approve: whoever releases must not be able to approve themselves through a second role. Approvals are assigned to a dedicated group (e.g. the change advisory board) with the approver role.
Application patterns are qualified: shop covers only the untenanted shop, payments/shop the one of the tenant payments, */* every application. A binding with tenants (a selector on the tags of the owner tenant and on name) applies only to the deploys of its applications; selector reads the application tags (only the keys of rbac.selector_keys). Tenant members and admins also get the implicit roles of rbac.tenant_roles.
Further roles are defined in rbac.roles:
Rules#
- Lists.
GET /deployments,GET /schedulesandGET /inventory(and the corresponding CLI commands) return only what the identity can read. The deploy list keeps going through older ones until it reacheslimit. - Denied action. The response is
403with the reason; in the CLI the command exits with the same error. - Schedules with the same name. A schedule is updated by name: if one with the same name already exists for another application or environment,
schedule.writeis also needed on that one. - Schedule deploys. Deploys generated by a schedule appear as requested by whoever last created or updated the schedule.
- Plan.
POST /deployments/planrequiresdeployment.read, because it reveals the pipeline of the application and environment. - Promotion. Requires
deployment.readon the source environment anddeployment.submiton the destination. - Approvals. Whoever requested a deploy cannot approve it, unless the rule has
allow_self. - Freezes. Freeze windows are visible to every identity with
deployment.readon at least one application, also the windows on other applications (a window on*blocks theirs too). An authenticated identity without permissions gets403. - Dependencies and releases. A deploy with
afterrequiresdeployment.readon its dependencies. A composite release requires the permissions of every deploy (plusdeployment.rollbackwithonFailure: rollback); reading or cancelling it requiresdeployment.readordeployment.cancelon all its deploys. - Activity and workers.
GET /activitylists only the deploys the identity can read.GET /workersrequiresplatform.read; of the running workflows it shows only those of readable applications and environments, and counts the others inhidden_tasks.
Client and server#
- The
manentclient always calls the API with the token fromMANENT_TOKENor the token file (see CLI). It contains neither the engine nor the database drivers and has no database credentials: authentication, permissions and audit are the server's. It is the tool to give users and CI pipelines. manent-server adminandmanent-server audit verifywork on the system database and the audit file, with the server's credentials. The identity recorded in the audit log is the operating system user (cli:<user>).
Database access is the real boundary
Whoever has the database credentials can act on DBOS directly, bypassing manent. Credentials go only to server processes and platform operators; users use the client.
Audit#
Every relevant action produces an audit entry:
| Action | When |
|---|---|
deployment.submit, deployment.dry-run |
Request of a deploy, even if denied or refused. |
deployment.promote, deployment.rollback |
Promotion and manual rollback. |
deployment.approve, deployment.reject |
An approver's decision. |
approval.finish |
Outcome of an approval (approved, rejected, expired), recorded by the worker. |
after.finish, release.finish |
Outcome of a dependency wait and of a composite release, recorded by the worker. |
deployment.cancel |
Cancellation. |
schedule.pause, schedule.resume, schedule.delete |
Schedule operations. |
freeze.create, freeze.delete |
Freeze windows created or deleted at runtime (also from manent-server admin freeze-delete). |
policy.push, policy.activate |
Governance policy revisions pushed and activated (also from manent-server admin policy-activate). |
deployment.read, schedule.read, schedule.write, platform.read |
Only if denied. |
deployment.finish |
Final outcome of the deploy, recorded by the worker. |
Outcome (outcome) |
Meaning |
|---|---|
allowed |
Action authorized and performed. |
denied |
Denied by permissions. |
error |
Authorized but refused (e.g. artifact source not allowed) or failed. |
succeeded, failed |
Final outcome of the deploy (deployment.finish). |
The comment field holds the comment of an approval or the reason for a freeze override.
{"time": "2026-10-09T21:42:46.02513Z", "actor": "token:ci-pipeline", "action": "deployment.submit",
"outcome": "denied", "application": "myapp", "environment": "prod",
"reason": "permission denied: token:ci-pipeline cannot perform deployment.submit on myapp/prod"}
Entries go:
- to the application log and hence to the SIEM (attribute
audit=true, componentaudit), where they must be retained according to company policy; - if
audit.fileis set, to a JSON Lines file opened append-only with0600permissions, synced to disk at every entry.
audit:
file: /var/log/manent/audit.jsonl
max_size_mb: 100 # rotation (0 = none)
hmac_key_ref: "vault:secret/data/manent/audit#key" # optional
File integrity#
The file's entries form a hash chain: each one carries the hash of the previous one (prev_hash) and its own (hash), computed on the line itself. Modifying, inserting, swapping or deleting an entry breaks the chain:
{"files": ["/var/log/manent/audit.jsonl.20261010T091200.000000000Z", "/var/log/manent/audit.jsonl"],
"entries": 1520, "last": "5b1f…"}
| Protection | How |
|---|---|
| Modifications and deletions | Detected by audit verify, with the line number. |
| Recomputing the chain | With hmac_key_ref the hash is an HMAC-SHA256: whoever modifies the file without the key cannot recompute it. Without a key the chain detects errors and clumsy changes, not an attacker who recomputes it. |
| Truncation of the last entries | Not detectable from the file alone: compare last with the copy in the SIEM or an archive. |
- Rotation. Once
max_size_mbis exceeded, the file is renamed with the suffix.<UTC date>and a new one is opened. The chain continues: the first entry of the new file carries the hash of the last one of the previous file. Archiving or deleting rotated files is up to company processes: if the first verified file does not start from the beginning,audit verifyreports itsprev_hashasanchor. - One file per process. Two processes must not write to the same file: each resumes the chain from the last entry only at startup. With several workers on the same host, use a different path for each.
- Authoritative copy. The SIEM remains the reference copy; the file is for local verification and as a second copy.
The deployment.finish entry is recorded in a DBOS step: after a recovery it is not repeated. It also reports the outcome of any rollback.
Who requested a deploy#
The identity that requested the deploy is saved in the DBOS workflow (the workflow's authenticated user). It appears:
- in the deploy status and in lists (
requested_by); - in every callback event (
requested_by); - in the worker's logs and in the
deployment.finishentry.