Skip to content

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:

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

  1. Navigate to: Project → Access Manager → API Keys → Create API Key
  2. Description: k8s-atlas-operator-preview-users
  3. Project Permissions: Select Project Database Access Admin

⚠️ NOT Project Owner - this is critical for security

  1. Note the Public Key and Private Key
  2. 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 externalProjectRef to 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:

  1. PR closes → GitHub webhook triggers
  2. ArgoCD deletes namespace pr-{N}
  3. Kubernetes garbage collects the AtlasDatabaseUser CR
  4. Atlas Operator detects CR deletion
  5. 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

  1. Open a test PR
  2. Wait for preview environment to deploy
  3. Check Atlas Console → Database Access for syrf_pr_{N}_app user
  4. Verify user has readWrite on syrf_pr_{N} only
  5. Close the PR
  6. 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 Admin role
  • 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