Accounts and identity federation¶
Two populations¶
| Population | Authentication | Scope |
|---|---|---|
| Provider staff | identity federation (SSO) or local account | every customer |
| Customers | local account only | one customer |
SSO is configured at platform level, not per customer: a single identity provider, the provider's own. There is no per-customer federation — a customer gets local accounts restricted to its own perimeter.
Roles¶
| Role | Scope | What it can do |
|---|---|---|
platform_admin |
all customers | everything, including creating customers and accounts |
platform_operator |
all customers | full operation, agents included |
tenant_admin |
its customer | inventory, credentials, retention, audit |
tenant_operator |
its customer | trigger backups, download |
tenant_readonly |
its customer | read |
platform_pending |
none | nothing — a holding role, see below |
What each role authorises, precisely¶
| Permission | readonly |
operator |
tenant_admin |
platform_operator |
platform_admin |
|---|---|---|---|---|---|
| See the inventory, the configurations, the reports | ✓ | ✓ | ✓ | ✓ | ✓ |
| Download a configuration | ✓ | ✓ | ✓ | ✓ | |
| Trigger a backup or a test | ✓ | ✓ | ✓ | ✓ | |
| Modify the inventory | ✓ | ✓ | ✓ | ||
| Manage credentials, approve a fingerprint | ✓ | ✓ | ✓ | ||
| Change retention and trust postures | ✓ | ✓ | ✓ | ||
| Read the audit log | ✓ | ✓ | ✓ | ||
| Manage agents | ✓ | ✓ | |||
| Create and withdraw customers, change the settings | ✓ | ||||
| Manage accounts | ✓ | ||||
| Destroy a key, force a purge | ✓ |
The platform_* roles are attached to no customer; the tenant_* roles
require one.
The provider staff's global access is deliberate: restricting it would break the managed-service model. Its counterpart is the logging of every view, with the customer concerned.
Local accounts¶
backup user ops@mssp.example.net '<password>' --role platform_admin
backup user customer@acme.example '<password>' --role tenant_admin --tenant acme
The password must be at least twelve characters, and is stored hashed (Argon2). A
tenant_* account must name its customer; a platform_* account names none.
Also via the API.
# Create a local account (requires “user:manage”) curl -s -X POST "$BASE/api/users" \ -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \ -d '{"email":"customer@acme.example","display_name":"ACME — operations", "role":"tenant_admin","tenant_id":"<customer-uuid>","password":"…"}' # List the accounts curl -s "$BASE/api/users" -H "Authorization: Bearer $TOKEN"
platform_pending cannot be assigned by hand: it is a holding role that only
identity federation produces.
The holding role¶
A member of staff who authenticates through SSO without belonging to any known
group gets platform_pending rather than an authentication refusal.
The sign-in succeeds, no data is accessible, and the screen displayed names what is missing: “account recognised by the directory but attached to no platform group”. That produces fewer support calls than an opaque error, and the person knows who to ask.
platform_pending is neither a platform role nor a customer role: it does
not lift isolation, holds no permission, and requires no attachment to a
customer.
Signing in through the API¶
TOKEN=$(curl -s -X POST "$BASE/api/auth/login" \
-H 'Content-Type: application/json' \
-d '{"email":"ops@mssp.example.net","password":"…"}' | jq -r .token)
curl -s "$BASE/api/auth/me" -H "Authorization: Bearer $TOKEN"
The token is valid for BKP_SESSION_TTL_MINUTES (480 min by default). Failed
attempts are logged, and the refusal message is identical whatever the
reason: it does not reveal whether the address exists.
See Using the API.
Identity federation (SSO)¶
Two protocols are supported. OIDC is recommended for Entra ID: it needs no system library, is far easier to diagnose, and covers exactly the same need as SAML here.
Trying the flow out without Entra ID¶
A laboratory OpenID Connect issuer is provided. It speaks the same protocol as Entra ID: what works here will work there, up to the tenant's own settings.
docker compose --profile sso up -d keycloak
BKP_SSO_PROVIDER=oidc \
BKP_OIDC_ISSUER=http://keycloak:8081/realms/backupmssp \
BKP_OIDC_CLIENT_ID=backupmssp \
BKP_OIDC_CLIENT_SECRET=secret-de-laboratoire \
BKP_PUBLIC_URL=http://localhost:8080 \
docker compose up -d platform
The realm is provisioned in advance: two groups (backup-admins,
backup-operators) and two accounts.
| Account | Password | Group | Expected role |
|---|---|---|---|
ops |
ops |
backup-admins |
platform_admin |
lecteur |
lecteur |
none | platform_pending |
Open http://localhost:8080 and click Sign in with SSO. The Keycloak
administration console is on http://localhost:8081 (admin / admin).
The second account is there to check the behaviour of the holding role described above.
Entra ID, in real conditions¶
1. Register the application¶
Azure portal → Microsoft Entra ID → App registrations → New registration.
- Name:
MSSP backup platform - Account types: Accounts in this organizational directory only
- Redirect URI: type Web, value
https://<your-domain>/api/auth/sso/callback
Take note of the Application (client) ID and the Directory (tenant) ID.
The redirect URI must match exactly, scheme and case included. Entra ID requires HTTPS, except for
http://localhost.
2. Create a client secret¶
Certificates & secrets → New client secret. Copy the value immediately: it is not displayed again afterwards. Note the expiry date — a secret that has run out shows up as sign-ins stopping overnight.
3. Emit the groups claim¶
This is the step most often forgotten, and without it everyone arrives with the holding role.
Token configuration → Add groups claim → tick Groups assigned to the application (rather than All groups), and for each token type choose Group ID or Display name as the source attribute.
Then Enterprise applications → your application → Users and groups → assign the groups concerned.
Entra ID returns object identifiers by default, not names. If you keep that setting,
BKP_SSO_ADMIN_GROUPmust contain the group's identifier, not its label.Beyond roughly 150 groups, Entra ID stops including the claim and returns an indirect reference: the role then falls back silently to holding. Assigning the groups explicitly to the application avoids that case.
4. Configure the platform¶
BKP_SSO_PROVIDER=oidc
BKP_OIDC_ISSUER=https://login.microsoftonline.com/<directory-tenant-ID>/v2.0
BKP_OIDC_CLIENT_ID=<application-ID>
BKP_OIDC_CLIENT_SECRET=<secret-value>
BKP_PUBLIC_URL=https://<your-domain>
BKP_SSO_GROUPS_CLAIM=groups
BKP_SSO_ADMIN_GROUP=<name or identifier of the administrator group>
BKP_SSO_OPERATOR_GROUP=<name or identifier of the operator group>
BKP_PUBLIC_URL is what builds the redirect URI: if it does not match what is
declared on the Entra ID side, sign-in fails with AADSTS50011.
The role is read again at every sign-in: removing somebody from a group takes effect at their next sign-in, with no action on the platform.
5. Verify¶
Must answer {"configured": true, "provider": "oidc", "available": true}. Then
sign in from the home page: the SSO link only appears if a provider is genuinely
reachable.
The audit log records every federated sign-in with the groups received and the role deduced. That is where you read why somebody arrived with the wrong role, instead of guessing.
Diagnosis¶
| Symptom | Probable cause |
|---|---|
AADSTS50011 |
redirect URI does not match — check BKP_PUBLIC_URL |
AADSTS7000215 |
client secret wrong or expired |
Everyone in platform_pending |
groups claim not emitted, or object identifier not matching BKP_SSO_ADMIN_GROUP — the Audit screen records the groups received; compare them |
| “no email address in the assertion” | profile and email scopes not granted |
| “state parameter does not match” | cookie lost — cookies blocked, or several tabs |
SAML 2.0 (ADFS)¶
Same principle, with two differences: the return point is
/api/auth/sso/callback by POST, and the python3-saml library requires
libxmlsec1 — present in the platform image, absent from an ordinary development
machine.
GET /api/auth/sso/status says explicitly whether the provider is operational,
rather than failing at the first exchange.
What to declare on each side¶
SAML compares what the two sides say. Three values must match exactly, and a mismatch is not a warning — the assertion is rejected.
| In your identity provider | Value | Where it comes from |
|---|---|---|
| Identifier / Entity ID | <BKP_PUBLIC_URL>/saml/metadata |
BKP_SAML_ENTITY_ID, or this default |
| Reply URL / ACS | <BKP_PUBLIC_URL>/api/auth/sso/callback |
fixed — it is the return route |
| Sign | the assertion | the default in Entra ID and ADFS |
BKP_SAML_METADATA_PATH points the other way: at the provider's own metadata,
as a file or a federation metadata URL. Its signing certificate and sign-in
address are read from there, so an expired or rotated certificate is picked up
by replacing that file, not by editing settings.
An Entity ID is an identifier, not an address: the convention gives it the shape of a URL without requiring it to lead anywhere. The Reply URL is the opposite — it is a real address, and it is the one route that consumes an assertion.
Behind a URL prefix, both values carry it: an instance published at
https://example.net/demo/ declares
https://example.net/demo/api/auth/sso/callback. Setting the prefix in
BKP_PUBLIC_URL, in BKP_ROOT_PATH, or in both gives the same result — it is
never added twice. Declaring the address without the prefix fails in a way worth
knowing: the assertion is rejected for a Destination mismatch, which reads as
a provider problem rather than as a missing prefix.
Unsigned assertions are refused, whatever the library's own default. A provider configured to sign only the response message, and not the assertion, will therefore be turned away; have it sign the assertion.
Single logout is not offered: no route consumes a LogoutRequest, so none is
advertised. Signing out of the platform ends the platform session only.