With bootstrap.enabled the chart rendered a PerconaServerMongoDBRestore without backupSource.s3.bucket. The operator CRD requires that field, so the API server rejected the restore and the HelmRelease failed. The prefix was missing too, and it matters just as much. For a backupSource restore the operator builds the PBM storage from backupSource.s3 alone and takes only the backup name from backupSource.destination, so a restore without the prefix would look for the backup at the bucket root. The restore now takes the bucket and prefix from the same helpers that fill the backup storage, so it reads from where the backups are written. Both places quote the two values, so a numeric bucket or prefix stays a string instead of making the resource invalid. The backup storage still leaves out an empty prefix, so running releases keep the same object. A destinationPath that names no bucket fails at render time instead of reaching the operator with an empty bucket. Assisted-by: LLM Signed-off-by: Yan Bondarenko <202671653+yankawai@users.noreply.github.com>
Managed MongoDB Service
MongoDB is a popular document-oriented NoSQL database known for its flexibility and scalability. The Managed MongoDB Service provides a self-healing replicated cluster managed by the Percona Operator for MongoDB.
Deployment Details
This managed service is controlled by the Percona Operator for MongoDB, ensuring efficient management and seamless operation.
- Docs: https://docs.percona.com/percona-operator-for-mongodb/
- Github: https://github.com/percona/percona-server-mongodb-operator
Deployment Modes
Replica Set Mode (default)
By default, MongoDB deploys as a replica set with the specified number of replicas. This mode is suitable for most use cases requiring high availability.
Sharded Cluster Mode
Enable sharding: true for horizontal scaling across multiple shards.
Each shard is a replica set, and mongos routers handle query routing.
Notes
TLS Mode
TLS is always enabled and fully managed by the PSMDB operator via its native cert-manager integration. The operator creates the complete certificate chain:
- A self-signed CA Issuer and CA Certificate
- A CA-backed Issuer
- A leaf TLS certificate with SANs covering the replica set service, per-pod DNS names, wildcards, and localhost
The chart does not render cert-manager Issuer or Certificate resources. The operator manages these directly, and chart-rendered cert-manager objects would be orphaned and ignored as trust anchors.
TLS mode is set to preferTLS, which means the MongoDB server accepts both TLS and non-TLS connections. This is also the operator's own default, so the chart states it explicitly rather than changing behaviour; spec.secrets.ssl is likewise set explicitly to the name the operator would default to.
Note:
preferTLSaccepts plaintext connections, so it authenticates the server to clients that opt in but does not enforce TLS. Moving torequireTLSis a breaking change for any existing plaintext client and is out of scope here. To enforce TLS at the network layer in the meantime, restrict client access via NetworkPolicy.The operator also defaults
tls.allowInvalidCertificatestotrue, and this chart leaves that default in place, so the server does not reject clients presenting an invalid or mismatched certificate. Combined withpreferTLS, the guarantee this configuration provides is that a client which opts into TLS can verify the server; it is not mutual authentication and it does not keep unverified clients out. Both are inherited operator defaults rather than choices made here, and tightening either is a separate breaking change.
External hostname SANs: When
external: trueis enabled, the chart advertises per-pod LoadBalancer hostnames to the operator viasplitHorizons, and the operator folds those hostnames into the leaf certificate SAN list. External clients can then verify the TLS hostname, provided<release>-rs0-<i>.<host>resolves to the corresponding per-pod LoadBalancer — DNS is an operational prerequisite you must satisfy.That fold is gated on two versions, and this chart controls only one of them. It sets
crVersion: 1.22.0itself, but the PSMDB operator must also be at 1.22.0 or newer, and the operator is a separate platform component (packages/system/mongodb-operator, currently 1.22.0) with its own lifecycle. On a platform carrying an older operator the gate is not met and nothing reports it: the chart still renderssplitHorizons, the CR is still accepted, the cluster still comes up healthy, and the external hostnames simply never enter the SAN list. A client that then enables hostname verification fails the handshake. If external verification does not work, check the operator version before suspecting DNS.The external hostnames are derived from
_namespace.host, which the Cozystack controller injects per tenant. Whenever it is empty, TLS stays enabled butsplitHorizonsis omitted, so the certificate does not cover external hostnames and a client connecting to the LoadBalancer with hostname verification on will fail the handshake. Nothing goes red when this happens — the render succeeds and the cluster comes up healthy.That empty state has two causes and only one of them heals. During the brief window before the controller populates the field (or when the chart is rendered outside Cozystack), it resolves on the next reconcile. But a tenant with no host of its own and no ancestor supplying one resolves to an empty host permanently —
packages/apps/tenantcomputes it from.Values.hostor the parent's, and leaves it empty when neither is set. On such a tenantexternal: trueworks for IP-based access and external TLS hostname verification is simply unavailable; set a host on the tenant (or an ancestor) if you need it. The chart does not fail the render in this case, because that would break existing hostless tenants already runningexternal: true.
Retrieving the CA bundle
The trust anchor is published as mongodb-<name>.tenant-ca: an object holding ca.crt and nothing else, created for every release and delivered to tenants through the core.cozystack.io/tenantsecrets API that the base tenant roles already grant. Every object below is named after the Helm release rather than the MongoDB resource, so a MongoDB named catest gives mongodb-catest.tenant-ca, mongodb-catest-ssl, and so on; <release> stands for mongodb-<name> throughout.
The chart declares where it is lifted from by rendering a TenantProjection naming <release>-ssl. That is the source rather than the operator's <release>-ca-cert because it is the name this chart sets in spec.secrets.ssl, the operator writes it on both its cert-manager path and its self-signed fallback, and its ca.crt carries the merged old and new CA through a rotation. Only ca.crt is ever published; the server private key alongside it stays where it is.
kubectl --context <ctx> --namespace <tenant> \
get tenantsecret mongodb-<name>.tenant-ca \
--output jsonpath='{.data.ca\.crt}' | base64 --decode
<release>.tenant-ca is the only object that hands over the CA certificate without also handing over a private key, which is why it exists. The PSMDB operator creates its own <release>-ca-cert Secret, but that one stores the CA private key alongside the certificate — read access would let the holder issue certificates for anything, so it is never granted to a tenant. The same applies to the leaf <release>-ssl, which holds the server private key.
It is reached through tenantsecrets rather than by reading the Secret directly, and that is deliberate: tenantsecrets surfaces only objects the platform has vouched for, whereas a direct grant on the name would convey whatever happens to occupy that name.
Warning: The CA rotates. The operator issues it with a 365-day duration and cert-manager renews it roughly 30 days before expiry, so a bundle copied once will stop verifying within a year. Re-read
<release>.tenant-caon a schedule (or mount it and let the kubelet refresh it) instead of bakingca.crtinto a client image, ConfigMap, or truststore built at release time.
Verifying a connection
Write the trust anchor to a file, then read the connection string out of <release>-credentials:
kubectl --context <ctx> --namespace <tenant> \
get tenantsecret mongodb-<name>.tenant-ca \
--output jsonpath='{.data.ca\.crt}' | base64 --decode > ca.crt
kubectl --context <ctx> --namespace <tenant> \
get secret mongodb-<name>-credentials \
--output jsonpath='{.data.uri}' | base64 --decode
mongosh takes the bundle as a file, and the two TLS inputs are passed as flags rather than folded into the URI:
mongosh "<uri>" --tls --tlsCAFile ./ca.crt
Drivers take the same two inputs under their own names (tls=true plus a CA path). Prefer the flag form over appending tls=true to the connection string: the replica-set URI already carries a ?replicaSet=rs0 query and the sharded one carries none, so the correct separator differs between the two topologies.
Omitting the TLS flags does not fail. Because the server runs in
preferTLS, a client that leaves them out is accepted on a plaintext connection and nothing reports the downgrade — the session simply is not encrypted. Verification is opt-in on the client side, so treat a working connection as evidence of nothing until the flags are present.
Hostname verification succeeds against the in-cluster names out of the box, since the operator covers them in the leaf SANs. Verifying against an external LoadBalancer endpoint depends on the splitHorizons fold described above, and is unavailable in sharded mode — see below.
Sharding and TLS
In-cluster mongos SANs are fully covered: the operator appends the mongos and config-replset names (short, namespace, FQDN, and wildcard forms) to every certificate, for sharded and non-sharded clusters alike.
Warning: External TLS hostname verification is not available when
sharding: true. In sharded mode external access goes through the single<release>-externalLoadBalancer frontingmongos, and no SAN covers that name —splitHorizonsis replica-set scoped and is only rendered for the non-sharded topology. Clients connecting to a sharded cluster from outside must either verify against an in-clustermongosname that resolves to the same endpoint, or connect without hostname verification. Combiningsharding: truewithexternal: trueand full verification is unsupported.
External Access
When external: true is enabled:
- Replica Set mode: The operator exposes each replica set member through its own LoadBalancer via
replsets[].exposeand rewrites the replica set configuration so drivers can discover the primary from outside the cluster. There is no single load-balanced endpoint: the driver connects to the members and routes writes to the primary itself, using the replica set connection string. - Sharded mode: Traffic is routed through mongos routers, which handle both reads and writes correctly.
Credentials
The chart generates the operator's system-user passwords and writes them to <release>-percona-server-mongodb-users before the operator starts, so <release>-credentials carries a working password and uri from the first install onwards. An upgrade reuses what the secret already holds and never rotates a live password.
A database created before this behaviour landed keeps the secret the operator generated for it, and its <release>-credentials is filled on the next upgrade of the release with the password the operator had already assigned.
Data lifecycle
When the MongoDB release is uninstalled, the operator finalizers reclaim release-scoped resources:
Reclaimed by the percona.com/delete-psmdb-pvc finalizer:
- All PVCs backing the replica set storage. Whether the underlying PersistentVolume and on-disk data are actually deleted depends on the StorageClass
reclaimPolicy(Deleteremoves them,Retainleaves them for manual cleanup). - Operator-managed secrets:
<release>-percona-server-mongodb-users— operator users credentialsinternal-<release>— internal operator stateinternal-<release>-users— operator-internal users data<release>-mongodb-encryption-key— at-rest encryption key
Reclaimed by helm uninstall:
<release>-credentials— connection string for application code<release>-user-<username>— per-user passwords<release>-s3-creds— backup destination credentials (if backups are configured)
Reclaimed by ownerReference garbage collection:
-
TLS secrets
<release>-ssl,<release>-ssl-internal, and<release>-ca-cert, along with the cert-managerIssuerandCertificateobjects the operator creates. The cascade works out either way on the--enable-certificate-owner-reffork: with the flag the Certificate owns the Secret and the CR owns the Certificate; without it (the default Cozystack ships) the operator makes the CR the direct owner of the Secret.The Secret ownerReference is applied at reconcile time, not at creation. cert-manager creates these Secrets unowned, and the operator patches the CR reference on afterwards, in
WaitForCerts. Two paths skip the patch: a dry-run reconcile returns before it, and a Secret already present under the same name whosecert-manager.io/certificate-nameannotation does not match is counted toward success and returned on without being patched. So a Secret that predates the release at one of these names — from a restore-from-backup, a rename, or a manual recreate — can stay unowned and survive uninstall. The finalizer does not cover the gap: it enumerates only the users,internal-<release>,internal-<release>-users, and encryption-key Secrets, and no TLS Secret is on that list. After deleting a release, confirm<release>-ca-certis gone rather than assuming it — it holds the CA private key, and an orphan of it is the one leak this chart's TLS posture exists to prevent. -
The
<release>.tenant-caprojection, where the CA-extraction controller is present to publish it, since it is owner-referenced to theTenantProjectionthe chart renders.
Not reclaimed automatically:
-
<release>-ssl-oldand<release>-ssl-internal-old, only if a rotation was interrupted. These are operator scratch state, not residue: the operator snapshots the previous leaf certificates under these names when it rotates the cert-manager chain, and deletes them itself in the same pass once the merged CA has landed in the live secrets. They are created with no ownerReference, so a rotation interrupted between the snapshot and the merge can leave them behind, and those stragglers do survive uninstall.Do not delete these while a rotation is in flight. The operator reads the previous CA back out of exactly these two names to build the merged trust bundle that keeps pods on old and new certificates talking to each other during the rolling restart. Removing them mid-rotation destroys the only copy of the old CA. Sweep them only if they are still present long after the cluster has settled, which indicates an interrupted rotation rather than one in progress.
Recovery from a stuck deletion:
If the psmdb-operator is uninstalled before MongoDB CRs are deleted, the finalizers cannot run and the PerconaServerMongoDB CR hangs in Terminating. To recover, clear the finalizers manually:
kubectl --namespace <namespace> patch psmdb <release> --type merge --patch '{"metadata":{"finalizers":[]}}'
Note that this skips the operator-driven cleanup — PVCs and operator-managed secrets will remain orphaned and must be removed manually.
If you need to retain data, take a backup before deletion. Refer to the Percona Operator for MongoDB documentation for backup/restore workflows.
Upgrading from earlier versions
Earlier versions of this chart referenced a namespace-shared system users secret (percona-server-mongodb-users). A release that scopes this secret per CR (<release>-percona-server-mongodb-users) renders the new secret on the next upgrade without rotating anything: while the per-release secret does not exist yet, the chart reads the current system-user passwords back from the operator's own copy, internal-<release>-users, and writes those same values into the new users secret and into <release>-credentials. The Percona operator sees unchanged values and leaves the running users alone; pods are not restarted and the cluster stays available.
Carried over on upgrade:
- The five operator-managed system accounts:
databaseAdmin,userAdmin,backup,clusterAdmin,clusterMonitorkeep their passwords. - Secret
<release>-percona-server-mongodb-usersis created per CR with the values already held byinternal-<release>-users. - Secret
<release>-credentialsis filled with the existingdatabaseAdminpassword and the matchinguri. Installs where these keys were empty get them on the next upgrade, because Helm re-renders only when the spec changes.
Not affected:
- Custom users defined under
users:in chart values. Their<release>-user-<name>secrets are not touched. - The at-rest encryption key (
<release>-mongodb-encryption-key) and replica set keyfile (<release>-mongodb-keyfile) are unchanged, so on-disk data remains readable.
Action required after upgrade:
Workloads that mounted <release>-credentials while its password and uri were empty see the filled values only after they re-read the secret. Restart those pods, or run a controller such as Reloader to roll them automatically.
Orphaned legacy secret:
The previous namespace-shared secret percona-server-mongodb-users is no longer referenced by any MongoDB CR after upgrade, but the operator does not garbage-collect it. If multiple MongoDB releases in the same namespace previously shared it, all of them rotate to their own per-CR secrets — passwords are no longer shared across CRs in the namespace, which is the intended outcome. Confirm no other consumers reference it, then remove it manually:
kubectl --namespace <namespace> delete secret percona-server-mongodb-users
Rolling restart on upgrade, for external replica set releases only:
A release already running with external: true in replica set mode gains splitHorizons on upgrade. The per-pod external hostnames become certificate SANs, so cert-manager reissues <release>-ssl; the operator hashes that Secret into a pod-template annotation, which rolls the replica set StatefulSet. The restart is ordered, and at replicas: 3 or more the replica set stays available through it — a member at a time goes down, the majority holds, and an election moves the primary if it was the one restarted. It is still a restart of the database and worth scheduling.
Below three replicas the upgrade is an outage, not a rolling restart. The chart permits
replicas: 1andreplicas: 2(renderingunsafeFlags.replsetSize: truefor both), and neither is excluded from the SAN change — a one-replica external release gets horizons and rolls exactly like a three-replica one. Atreplicas: 1the rollout takes the only mongod down, so the release is fully unavailable, reads included, until the new pod is ready. Atreplicas: 2the write majority is still 2, so restarting either member leaves the remaining one unable to hold a primary and writes fail for the duration; reads continue only if the client tolerates a secondary.podDisruptionBudgetdoes not change this in either case — the disruption is the rollout itself, not an eviction. Schedule a window accordingly, or scale to three before upgrading.
The same pass snapshots the superseded certificates as <release>-ssl-old and <release>-ssl-internal-old, then deletes them once the merged CA has landed. Because this upgrade changes only the SAN list and not the CA itself, the merge is a no-op and both snapshots are removed on the first pass — no cleanup is expected. See the lifecycle section above for the interrupted case, and do not delete them by hand while the rollout is running.
Scaling an external replica set down can also produce transient reconfiguration errors during the upgrade. MongoDB requires horizons on all members or none, and a terminating pod is still a member while the chart advertises horizons only for the remaining ones. The operator retries and the condition clears once the pod is gone.
Releases with external: false, and sharded releases in any configuration, are unaffected: neither renders splitHorizons, so no SAN changes and nothing restarts.
storageClassis annotated as immutable in the chart schema — seedocs/storage-immutability.mdfor the contract and which consumers enforce it.
Parameters
Common parameters
| Name | Description | Type | Value |
|---|---|---|---|
replicas |
Number of MongoDB replicas in replica set. | int |
3 |
resources |
Explicit CPU and memory configuration for each MongoDB replica. When omitted, the preset defined in resourcesPreset is applied. |
object |
{} |
resources.cpu |
CPU available to each replica. | quantity |
"" |
resources.memory |
Memory (RAM) available to each replica. | quantity |
"" |
resourcesPreset |
Default sizing preset used when resources is omitted. |
string |
t1.small |
size |
Persistent Volume Claim size available for application data. | quantity |
10Gi |
storageClass |
StorageClass used to store the data. | string |
"" |
external |
Enable external access from outside the cluster. | bool |
false |
version |
MongoDB major version to deploy. | string |
v8 |
Sharding configuration
| Name | Description | Type | Value |
|---|---|---|---|
sharding |
Enable sharded cluster mode. When disabled, deploys a replica set. | bool |
false |
shardingConfig |
Configuration for sharded cluster mode. | object |
{} |
shardingConfig.configServers |
Number of config server replicas. | int |
3 |
shardingConfig.configServerSize |
PVC size for config servers. | quantity |
3Gi |
shardingConfig.mongos |
Number of mongos router replicas. | int |
2 |
shardingConfig.shards |
List of shard configurations. | []object |
[...] |
shardingConfig.shards[i].name |
Shard name. | string |
"" |
shardingConfig.shards[i].replicas |
Number of replicas in this shard. | int |
0 |
shardingConfig.shards[i].size |
PVC size for this shard. | quantity |
"" |
Users configuration
| Name | Description | Type | Value |
|---|---|---|---|
users |
Users configuration map. | map[string]object |
{} |
users[name].password |
Password for the user (auto-generated if omitted). | string |
"" |
Databases configuration
| Name | Description | Type | Value |
|---|---|---|---|
databases |
Databases configuration map. | map[string]object |
{} |
databases[name].roles |
Roles assigned to users. | object |
{} |
databases[name].roles.admin |
List of users with admin privileges (readWrite + dbAdmin). | []string |
[] |
databases[name].roles.readonly |
List of users with read-only privileges. | []string |
[] |
Backup parameters
| Name | Description | Type | Value |
|---|---|---|---|
backup |
Backup configuration. | object |
{} |
backup.enabled |
Enable regular backups. | bool |
false |
backup.useSystemBucket |
Opt-in: back up to the platform system bucket without supplying S3 credentials. The cozy-default MongoDB BackupClass injects the storage at BackupJob time; tenants do not set s3AccessKey/s3SecretKey/destinationPath/endpointURL, and backups scope to <namespace>/<application>. Scheduling moves to a BackupClass Plan; schedule, retentionPolicy and PITR are ignored while this is on, and this flow has no retention yet (see docs/operations/backup-classes.md). Requires backup.enabled: true (the pbm agents run on that flag); the chart refuses the combination with enabled: false at render time; the schema carries the same rule as a marker for the UI and for the day the apiserver enforces CEL on application schemas (cozystack/cozystack#2657), which it does not yet. A release that has not yet rendered this flag (its cluster still carries the chart's scheduled tasks and PITR) holds a BackupJob until it does, so the tenant's own storage is never injected over. |
bool |
false |
backup.schedule |
Cron schedule for automated backups. Ignored when useSystemBucket is true (scheduling moves to the BackupClass Plan). | string |
0 2 * * * |
backup.retentionPolicy |
Retention policy (e.g. "30d"). Ignored when useSystemBucket is true (no engine-side retention on that flow). | string |
30d |
backup.destinationPath |
Destination path for backups (e.g. s3://bucket/path/). Ignored when useSystemBucket is true (the driver injects the platform bucket). | string |
s3://bucket/path/to/folder/ |
backup.endpointURL |
S3 endpoint URL for uploads. Ignored when useSystemBucket is true (the strategy supplies the platform endpoint). | string |
http://minio-gateway-service:9000 |
backup.insecureSkipTLSVerify |
Skip TLS verification for the S3 endpoint (self-signed, e.g. in-cluster seaweedfs). Ignored when useSystemBucket is true (the platform backupStorage knob applies instead). | bool |
false |
backup.s3AccessKey |
Access key for S3 authentication. Ignored when useSystemBucket is true (credentials come from the projected cozy-backups-creds). | string |
"" |
backup.s3SecretKey |
Secret key for S3 authentication. Ignored when useSystemBucket is true (credentials come from the projected cozy-backups-creds). | string |
"" |
Bootstrap (recovery) parameters
| Name | Description | Type | Value |
|---|---|---|---|
bootstrap |
Bootstrap configuration. | object |
{} |
bootstrap.enabled |
Whether to restore from a backup. | bool |
false |
bootstrap.recoveryTime |
Timestamp for point-in-time recovery; empty means latest. | string |
"" |
bootstrap.backupName |
Name of backup to restore from. | string |
"" |