MongoDB Atlas Kubernetes Operator Setup¶
Overview¶
This guide covers setting up the MongoDB Atlas Kubernetes Operator to automatically provision per-PR database users. Each PR preview gets an isolated user that can only access its own database.
Related Documents:
- Technical Plan - Full implementation strategy
- Manual Setup - Production/staging user creation
- Security Model - Defense-in-depth architecture
Why Use the Operator for PR Previews?¶
Problem: Using a single syrf_preview_app user with readWriteAnyDatabase would allow PR code to access production (syrftest).
Solution: The Atlas Operator creates per-PR users scoped to their specific database:
PR #123 opens
↓
ArgoCD creates namespace pr-123 + AtlasDatabaseUser CR
↓
Atlas Operator creates user: syrf_pr_123_app
with access to: syrf_pr_123 ONLY (not syrftest!)
↓
Operator creates K8s Secret with connection string
↓
App reads secret, connects to MongoDB
PR #123 closes
↓
Namespace deleted → CR garbage collected
↓
Operator deletes user from Atlas
Prerequisites¶
- Kubernetes cluster with ArgoCD
- MongoDB Atlas project with existing cluster
- GCP Secret Manager for credential storage
- Policy engine installed (OPA Gatekeeper or Kyverno) - see Security Model
Step 1: Install Atlas Operator¶
Reference: MongoDB Atlas Operator Documentation
Install via GitOps (recommended) by adding to cluster-gitops:
# cluster-gitops/apps/atlas-operator.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: atlas-operator
namespace: argocd
spec:
project: default
source:
repoURL: https://mongodb.github.io/helm-charts
chart: mongodb-atlas-operator
targetRevision: 2.3.1
helm:
values: |
watchNamespaces: "pr-*"
atlasURI: https://cloud.mongodb.com
destination:
server: https://kubernetes.default.svc
namespace: atlas-operator
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
Critical Configuration: watchNamespaces: "pr-*" ensures the operator only monitors PR namespaces. It will ignore any AtlasDatabaseUser resources in production or staging namespaces.
Step 2: Create Atlas API Key (Least Privilege)¶
Reference: MongoDB Atlas User Roles
Create an API key with minimum permissions for managing PR preview users only.
In Atlas Console¶
- Navigate to: Project → Access Manager → API Keys → Create API Key
- Description:
k8s-atlas-operator-preview-users - Project Permissions: Select
Project Database Access Admin
⚠️ NOT Project Owner - this is critical for security
- Note the Public Key and Private Key
- Add GKE cluster IP ranges to API Key Access List
Why Project Database Access Admin?¶
| Permission | Project Owner | Project Database Access Admin |
|---|---|---|
| Manage database users | ✅ | ✅ |
| Manage custom roles | ✅ | ✅ |
| Modify cluster config | ✅ | ❌ |
| Delete clusters | ✅ | ❌ |
| Modify project settings | ✅ | ❌ |
| Manage backups | ✅ | ❌ |
With Project Database Access Admin, even if the API key is compromised, the attacker cannot delete the cluster or modify project settings.
Step 3: Store API Key in GCP Secret Manager¶
# Create the secret
gcloud secrets create atlas-operator-api-key \
--replication-policy="automatic" \
--project=camarades-net
# Add the secret value
gcloud secrets versions add atlas-operator-api-key \
--data-file=- \
--project=camarades-net << 'EOF'
{
"orgId": "YOUR_ATLAS_ORG_ID",
"publicApiKey": "YOUR_PUBLIC_KEY",
"privateApiKey": "YOUR_PRIVATE_KEY"
}
EOF
Create ExternalSecret¶
# cluster-gitops/atlas-operator/external-secret.yaml
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: atlas-operator-api-key
namespace: atlas-operator
spec:
refreshInterval: 1h
secretStoreRef:
kind: ClusterSecretStore
name: gcp-secret-store
target:
name: atlas-operator-api-key
creationPolicy: Owner
data:
- secretKey: orgId
remoteRef:
key: atlas-operator-api-key
property: orgId
- secretKey: publicApiKey
remoteRef:
key: atlas-operator-api-key
property: publicApiKey
- secretKey: privateApiKey
remoteRef:
key: atlas-operator-api-key
property: privateApiKey
Step 4: Configure Atlas Project Reference¶
CRITICAL: We use
externalProjectRefto reference the existing Atlas project. The operator will NOT manage or be able to delete the project itself.
Get your Atlas Project ID from: Project Settings → Project ID
# cluster-gitops/atlas-operator/project-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: atlas-project-config
namespace: atlas-operator
data:
projectId: "YOUR_ATLAS_PROJECT_ID"
Step 5: Deploy Policy Enforcement¶
⚠️ MANDATORY: Deploy policies BEFORE the operator goes live. See Security Model for full details.
Choose either OPA Gatekeeper or Kyverno to enforce that PR users can only access syrf_pr_* databases.
Quick Kyverno Policy¶
# cluster-gitops/policies/atlas-pr-user-policy.yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: block-atlas-production-access
spec:
validationFailureAction: Enforce
background: false
rules:
- name: require-pr-database
match:
any:
- resources:
kinds:
- AtlasDatabaseUser
namespaces:
- "pr-*"
validate:
message: "PR preview users must only access syrf_pr_* databases"
pattern:
spec:
roles:
- databaseName: "syrf_pr_*"
Step 6: Update PR Preview ApplicationSet¶
Add AtlasDatabaseUser creation to the PR preview workflow. Update syrf-previews.yaml:
# In cluster-gitops/argocd/applicationsets/syrf-previews.yaml
# Add to the template for services that need MongoDB access
# AtlasDatabaseUser CR - created per PR namespace
apiVersion: atlas.mongodb.com/v1
kind: AtlasDatabaseUser
metadata:
name: db-user
namespace: pr-{{.prNumber}}
annotations:
# CRITICAL: Allow deletion when PR closes
atlas.mongodb.com/deletion-protection: "false"
spec:
# Reference external project (NOT managed by operator)
externalProjectRef:
id: "YOUR_ATLAS_PROJECT_ID"
# API credentials for operator
connectionSecret:
name: atlas-operator-api-key
namespace: atlas-operator
# User configuration
username: syrf_pr_{{.prNumber}}_app
databaseName: admin # Auth database (required for SCRAM-SHA)
# CRITICAL: Only access to THIS PR's database
roles:
- roleName: readWrite
databaseName: syrf_pr_{{.prNumber}}
# Password reference
passwordSecretRef:
name: mongodb-password
Step 7: Password Management¶
Option A: Template Secret (Simpler)¶
Create a password secret that's copied to each PR namespace:
# Add to pr-preview workflow
apiVersion: v1
kind: Secret
metadata:
name: mongodb-password
namespace: pr-{{.prNumber}}
labels:
atlas.mongodb.com/type: credentials
type: Opaque
stringData:
password: "{{.generatedPassword}}" # Generated by workflow
Option B: Operator-Generated (Automatic)¶
The operator automatically creates a secret with the connection string after user creation:
- Secret name format:
{project}-{cluster}-{username} - Contains:
connectionStringStandard,connectionStringStandardSrv,username,password
Update Helm charts to read from this operator-generated secret.
Step 8: Update Helm Charts¶
Configure services to use the operator-created credentials:
# In service values.yaml
mongoDb:
# Use operator-generated secret
authSecretName: "cluster0-syrf-pr-{{ .prNumber }}-app"
# Or template secret
# authSecretName: mongodb-password
databaseName: "syrf_pr_{{ .prNumber }}"
Automatic Cleanup¶
When a PR closes, cleanup happens automatically:
- PR closes → GitHub webhook triggers
- ArgoCD deletes namespace
pr-{N} - Kubernetes garbage collects the
AtlasDatabaseUserCR - Atlas Operator detects CR deletion
- Operator deletes user from Atlas
The database itself remains (empty, no cost). For periodic cleanup of orphaned databases, see Database Cleanup.
Database Cleanup¶
Orphaned databases (user deleted but DB remains) can be cleaned with:
// Connect with admin credentials
// Run in mongosh or Atlas Data Explorer
db.adminCommand({ listDatabases: 1 }).databases
.filter(d => d.name.startsWith('syrf_pr_'))
.forEach(d => {
print('Dropping: ' + d.name);
db.getSiblingDB(d.name).dropDatabase();
});
Consider running as a scheduled CronJob monthly.
Verification¶
Test Policy Enforcement¶
Try creating a malicious CR (should be blocked):
# This SHOULD be rejected by policy
apiVersion: atlas.mongodb.com/v1
kind: AtlasDatabaseUser
metadata:
name: test-malicious
namespace: pr-test
spec:
roles:
- roleName: readWrite
databaseName: syrftest # BLOCKED - production database
Expected: Policy engine rejects the CR.
Test User Creation¶
- Open a test PR
- Wait for preview environment to deploy
- Check Atlas Console → Database Access for
syrf_pr_{N}_appuser - Verify user has
readWriteonsyrf_pr_{N}only - Close the PR
- Verify user is deleted from Atlas
Troubleshooting¶
User Not Created¶
- Check operator logs:
kubectl logs -n atlas-operator deployment/atlas-operator - Verify API key permissions: Must be
Project Database Access Admin - Check API key access list includes GKE IPs
Policy Rejection¶
- Check policy engine logs
- Verify CR matches expected pattern (
syrf_pr_*database) - Check namespace matches
pr-*pattern
Connection Failed¶
- Verify secret was created by operator
- Check secret contains correct connection string
- Verify database name matches in Helm values
Checklist¶
One-Time Setup¶
- Install MongoDB Atlas Operator via GitOps
- Create Atlas API key with
Project Database Access Adminrole - Store API key in GCP Secret Manager
- Create ExternalSecret for API key
- Get Atlas Project ID and create ConfigMap
- Deploy policy engine (OPA Gatekeeper or Kyverno)
- Deploy policy to block production database access
- Test policy by attempting malicious CR creation
ApplicationSet Updates¶
- Add AtlasDatabaseUser template to syrf-previews.yaml
- Configure password management approach
- Update Helm charts to read operator secrets
Verification¶
- Test with actual PR preview
- Verify user creation in Atlas
- Verify user scoped to correct database only
- Verify cleanup on PR close