Backup Visibility and Access¶
The portal can show the backups of a service instance and hand out the credentials
of the instance's backup bucket. Both are driven by the backup_config JSON on the
Service Definition in the Django admin. This page covers what staff need to
enable and operate the feature. The end-user view is described in
Backups & Restore.
Design notes and the staff runbook live in the portal repository:
docs/plans/2026-08-21-backup-download-design.md and
docs/runbooks/backup-access.md.
How it works¶
The portal stays on the control path only. It reads the backup discovery object in
the instance namespace (a K8up Schedule or a Barman ObjectStore), resolves the
endpoint, bucket and the names of the credential secrets from it, reads those
secrets with the control plane credentials, and renders a command block for the
matching tool. No backup data flows through the portal.
Tenancy is derived, never supplied: the only client input is the instance. Namespace,
bucket, endpoint and credentials are resolved server-side, and backup_config
deliberately has no namespace or bucket key.
Every bucket is created per instance by AppCat with its own key pair, so the blast
radius of a revealed credential is exactly one instance. The credentials are
read-write today: a holder can delete or modify every backup of that instance, and
there is no object lock. Read-only keys are planned as an AppCat contribution. When
a -ro suffixed credentials secret exists next to the read-write one, the portal
prefers it automatically and the warning in the UI de-escalates.
Prerequisites¶
Kubernetes RBAC¶
The portal's ClusterRole on each control plane must be able to list the discovery
objects, in addition to the secrets get it already has:
barmancloud.cnpg.io/objectstores(PostgreSQL)postgresql.cnpg.io/clusters(PostgreSQL, to derive the Barman server name)k8up.io/schedules(Redis, MariaDB, Nextcloud files, Codey)
Without these the panel shows "currently unavailable" and no credentials are shown. See Kubernetes RBAC.
Endpoint allowlist¶
The portal renders the endpoint into commands the user will run, and the endpoint
comes from a Kubernetes object the portal did not write. Only endpoints listed in
the SERVALA_BACKUP_ENDPOINT_ALLOWLIST environment variable (comma-separated
hostnames) produce a panel. Entries are exact hostnames or a *. wildcard covering
exactly one label, as TLS wildcards do: *.exo.io matches sos-ch-dk-2.exo.io but
not exo.io or a.b.exo.io. A bare exo.io matches only itself. The endpoint must
use HTTPS; private and link-local addresses are rejected.
Which backends are approved for direct customer access is a product decision. Check the actual endpoint of the instances you enable rather than assuming a provider.
Configuring a service definition¶
backup_config has a list of sources, one per backup mechanism, and a
service_label. Each source describes how to list backups (api, fields,
policy, and so on). Adding an access block to a source enables the Direct
backup access panel for it; sources without one only get the listing.
K8up services (Redis, MariaDB, Nextcloud files, Codey)¶
{
"sources": [
{
"label": "Database",
"api": {"group": "k8up.io", "version": "v1", "plural": "snapshots"},
"fields": {"stopped_at": "spec.date"},
"policy": {"schedule": "spec.parameters.backup.schedule"},
"access": {
"tool": "restic",
"api": {"group": "k8up.io", "version": "v1", "plural": "schedules"},
"endpoint": "spec.backend.s3.endpoint",
"bucket": "spec.backend.s3.bucket",
"access_key_id": "spec.backend.s3.accessKeyIDSecretRef",
"secret_access_key": "spec.backend.s3.secretAccessKeySecretRef",
"repo_password": "spec.backend.repoPasswordSecretRef"
}
}
],
"service_label": "<service label>"
}
PostgreSQL (CloudNativePG with the Barman Cloud plugin)¶
{
"sources": [
{
"label": "Database",
"api": {"group": "postgresql.cnpg.io", "version": "v1", "plural": "backups"},
"fields": {
"started_at": "status.startedAt",
"stopped_at": "status.stoppedAt",
"status": "status.phase"
},
"scheduled_label": "cnpg.io/scheduled-backup",
"success_values": ["completed"],
"failure_values": ["failed", "walArchivingFailing"],
"policy": {
"schedule": "spec.parameters.backup.schedule",
"retention": "spec.parameters.backup.retention"
},
"access": {
"tool": "barman",
"api": {"group": "barmancloud.cnpg.io", "version": "v1", "plural": "objectstores"},
"endpoint": "spec.configuration.endpointURL",
"bucket": "spec.configuration.destinationPath",
"access_key_id": "spec.configuration.s3Credentials.accessKeyId",
"secret_access_key": "spec.configuration.s3Credentials.secretAccessKey",
"server_name": {
"api": {"group": "postgresql.cnpg.io", "version": "v1", "plural": "clusters"},
"plugin": "barman-cloud.cloudnative-pg.io",
"parameter": "serverName"
}
}
}
],
"service_label": "postgresql"
}
The Barman server name is read from the CNPG Cluster's plugin parameters, which is
the value the backup tooling itself uses. Do not derive it from the image name:
AppCat pins images through an image catalog, so spec.imageName is empty.
Multi-source services (Nextcloud)¶
Nextcloud's own definition carries the K8up access block for its files (label it
"Files") and "dependent_labels": ["postgresql"]. The PostgreSQL dependent's panel
comes from the PostgreSQL definition through the dependent namespace.
Optional command templates¶
access.commands overrides the default command block for the tool. Placeholders are
{endpoint}, {bucket}, {access_key_id}, {secret_access_key}, {repo_password}
(restic) and {server_name} (barman). Every value is quoted for the shell when it is
substituted; a template that wraps a placeholder in quotes of its own is rejected on
save.
When a configured instance still shows no panel¶
Beyond RBAC and the endpoint allowlist, two gates are silent to the user:
- Customer-owned buckets. An instance with
unmanagedBucketanywhere in its spec (including the PostgreSQL dependent of Nextcloud and Keycloak) gets no panel at all. Those credentials are the customer's own and may reach across their whole cloud account, so the per-instance blast radius does not hold. Point the customer at their own bucket credentials. - A blank or half-finished access block. A block that is blank throughout is
simply no direct access. A block with paths but no
tool, or with a missing discovery resource orrepo_passwordpath, is rejected on save and logged.
Permissions, auditing and notifications¶
- Only organization administrators, owners and staff can reveal credentials
(
core.access_backup_credentials_serviceinstance). Members see the panel with a note to ask an administrator. - Every reveal is recorded as an audit log entry ("revealed backup credentials for ...") with requester, instance, source and source IP. It is visible in the organization's audit log under Access and in the Django admin.
- Organization owners are emailed the first time a non-staff user reveals the credentials of an instance. Staff reveals are audited but neither notify nor count as the first reveal, so support work does not raise alarms.
- Credential values never reach request logs, Sentry, or background task arguments.
The reveal endpoint is a CSRF-protected POST with
Cache-Control: private, no-store.
Rotating credentials after a leak or offboarding¶
Credentials are static and cannot be revoked from the portal. When a customer asks for replacement (offboarding, suspected leak, unexpected first-reveal notification):
- Bucket key: delete the
IAMKey(Exoscale) orObjectsUser(cloudscale) custom resource for the instance's bucket. The provider deletes the key at the cloud provider, recreates it, and rewrites the secret under the same name. K8up and CNPG reference the secret by name, so backups keep working. Revealed credentials stop working immediately. - Restic repository password (K8up services): run
restic key passwdwith read-write access to the repository, then update thek8up-repository-passwordsecret in the instance namespace. - Verify by running the portal-rendered command block with the old credentials: it must fail.
Before responding to an unexpected notification, check the audit log entry for requester and source IP.