← All docs

Obsidian VaultGuard - Deployment Guide

Complete guide for deploying the Obsidian VaultGuard backend infrastructure and distributing the plugin to your team.

Current documentation map: INDEX.md. For vault-scoped API paths, treat VAULTS.md and API.md as the source of truth.


Table of Contents


Prerequisites

Required Tools

Tool Version Purpose
AWS CLI >= 2.x AWS account interaction
Node.js >= 18.x Lambda bundling and plugin build
Terraform >= 1.6 Infrastructure deployment
npm or pnpm Latest Package management
Git >= 2.x Source control
Obsidian >= 1.4.0 Plugin host

AWS Account Requirements

  • An AWS account with administrator access (or at minimum, permissions for IAM, Cognito, DynamoDB, S3, Lambda, API Gateway, CloudWatch, KMS, and SES)
  • A registered domain for the API endpoint (optional, but recommended)
  • AWS CLI configured with credentials: aws configure

Verify Prerequisites

# Check all tools are installed
aws --version        # AWS CLI v2.x
node --version       # v18+
terraform --version  # 1.6+
npm --version        # 9+
git --version        # 2.x

AWS Infrastructure Deployment

The infrastructure is defined in terraform/. Lambda source lives in infrastructure/lambda/ and is bundled to infrastructure/dist/ by npm run build:lambdas before each apply.

1. Clone and Build Lambdas

git clone https://github.com/peter70700/vaultguard-obsidian.git
cd vaultguard-obsidian

# Bundle Lambda handlers (writes infrastructure/dist/{name}/handler.js)
cd infrastructure && npm install && npm run build:lambdas
cd ..

2. Configure tfvars

terraform/environments/ ships dev.tfvars and prod.tfvars. Edit the file matching your stage and set domain_name, admin_email, sender_email, and any other variables flagged in terraform/variables.tf.

3. Remote State Backend (one-time)

Terraform state is stored in a versioned + encrypted S3 bucket with DynamoDB locking (configured in terraform/versions.tf). Do this once per AWS account, before the first terraform init.

Already running on the local backend? See "Migrating existing local state" below — the migration copies state only and changes zero AWS resources.

a. Create the backend (bucket + lock table):

# Idempotent — safe to re-run; creates nothing that already exists.
./scripts/bootstrap-tf-backend.sh

This creates the S3 state bucket (vaultguard-tfstate-eu-central-1, versioned, AES256-encrypted, public access blocked, TLS-only) and the DynamoDB lock table (vaultguard-terraform-locks) in eu-central-1. If the bucket name is already taken globally, override and update terraform/versions.tf to match:

STATE_BUCKET=your-unique-name LOCK_TABLE=your-locks ./scripts/bootstrap-tf-backend.sh

b. Migrating existing local state (if you previously used the local backend):

⚠️ CRITICAL — always pass the stack's REAL stage. The live production stack runs with stage=dev (see the warning below). You MUST verify with -var="stage=dev" -var-file="environments/dev.tfvars". Passing stage=prod here produces a plan that destroys all production data (every resource name is derived from ${var.stage}). Never run migration verification with a stage value that differs from the deployed stack.

cd terraform
terraform init -migrate-state   # copies terraform.tfstate → S3; prompts to confirm
terraform plan -var="stage=dev" -var-file="environments/dev.tfvars"  # MUST report: No changes

-migrate-state only moves where state is stored — it does not touch any deployed AWS resource. The follow-up plan (with the correct stage=dev) must report "No changes". If it proposes to create/destroy anything — especially a large N to destroy count — stop immediately and do NOT apply: you almost certainly passed the wrong stage.

Do not change stage to make the name "honest." The production stack's stage variable is dev for historical reasons but it is production (vaultguard.cloud). Almost every resource name is derived from ${var.stage}, so a plan/apply with -var="stage=prod" would recreate and destroy the live vaults, users, subscriptions, and audit history (a ~117-resource destroy). The remote-state key is named prod purely for clarity; the stage var stays dev. For THIS stack, every terraform command uses stage=dev/dev.tfvars.

4. Initialize and Plan

cd terraform
terraform init
terraform plan -var="stage=prod" -var-file="environments/prod.tfvars"

5. Apply

terraform apply -var="stage=prod" -var-file="environments/prod.tfvars"

6. Note Outputs

terraform output

Critical values: api_url, cognito_user_pool_id, cognito_client_id, vault_bucket_name, cloudfront_url. Save these — they are needed for plugin and admin-panel configuration.

7. Re-deploying After Code Changes

When Lambda source changes, rebuild before the apply:

cd infrastructure && npm run build:lambdas && cd ..
cd terraform && terraform apply -var="stage=prod" -var-file="environments/prod.tfvars"

The archive_file data sources in terraform/modules/lambda/main.tf re-zip the new dist/ output and Terraform detects the hash change.

8. Verify

scripts/verify-deployment.sh cross-checks deployed Lambdas, API Gateway routes, and CORS coverage against the expected set:

./scripts/verify-deployment.sh --stage prod

Cognito Configuration

User Pool Settings

The Terraform deployment (terraform/modules/cognito/) creates a Cognito User Pool with these defaults:

  • Password policy: minimum 12 characters, requires uppercase, lowercase, numbers, symbols
  • MFA: optional (can be enforced via admin settings)
  • Email verification required
  • Account recovery via email

Adding SSO Providers (Optional)

Google Workspace

aws cognito-idp create-identity-provider \
  --user-pool-id YOUR_USER_POOL_ID \
  --provider-name Google \
  --provider-type Google \
  --provider-details \
    client_id=YOUR_GOOGLE_CLIENT_ID,\
    client_secret=YOUR_GOOGLE_CLIENT_SECRET,\
    authorize_scopes="openid email profile" \
  --attribute-mapping \
    email=email,\
    name=name,\
    picture=picture

Microsoft Entra ID (Azure AD)

aws cognito-idp create-identity-provider \
  --user-pool-id YOUR_USER_POOL_ID \
  --provider-name AzureAD \
  --provider-type OIDC \
  --provider-details \
    client_id=YOUR_AZURE_CLIENT_ID,\
    client_secret=YOUR_AZURE_CLIENT_SECRET,\
    oidc_issuer="https://login.microsoftonline.com/YOUR_TENANT_ID/v2.0",\
    authorize_scopes="openid email profile" \
  --attribute-mapping \
    email=email,\
    name=name

Okta

aws cognito-idp create-identity-provider \
  --user-pool-id YOUR_USER_POOL_ID \
  --provider-name Okta \
  --provider-type OIDC \
  --provider-details \
    client_id=YOUR_OKTA_CLIENT_ID,\
    client_secret=YOUR_OKTA_CLIENT_SECRET,\
    oidc_issuer="https://YOUR_ORG.okta.com",\
    authorize_scopes="openid email profile" \
  --attribute-mapping \
    email=email,\
    name=name

Custom Domain for Auth (Optional)

aws cognito-idp create-user-pool-domain \
  --user-pool-id YOUR_USER_POOL_ID \
  --domain auth.your-company.com \
  --custom-domain-config CertificateArn=arn:aws:acm:us-east-1:ACCOUNT:certificate/CERT_ID

Initial Admin Setup

1. Create the First Admin User

# Create user in Cognito
aws cognito-idp admin-create-user \
  --user-pool-id YOUR_USER_POOL_ID \
  --username admin@your-company.com \
  --user-attributes \
    Name=email,Value=admin@your-company.com \
    Name=email_verified,Value=true \
    Name=custom:role,Value=admin \
  --temporary-password "TempP@ss123!"

# Set permanent password (user will be prompted to change on first login)
aws cognito-idp admin-set-user-password \
  --user-pool-id YOUR_USER_POOL_ID \
  --username admin@your-company.com \
  --password "YourSecurePassword123!" \
  --permanent

2. Initialize Organization

# Call the initialization endpoint
curl -X POST https://YOUR_API_ENDPOINT/admin/init \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_ADMIN_TOKEN" \
  -d '{
    "orgName": "Your Company",
    "allowedDomains": ["your-company.com"],
    "defaultRole": "viewer",
    "enforceEncryption": true,
    "requireMfa": false
  }'

3. Generate Organization API Keys

curl -X POST https://YOUR_API_ENDPOINT/admin/api-keys \
  -H "Authorization: Bearer YOUR_ADMIN_TOKEN" \
  -d '{
    "name": "plugin-distribution",
    "scopes": ["plugin:configure"]
  }'

Plugin Installation

For Admins (Manual Install)

  1. Download the latest release from the releases page
  2. Extract to your vault's plugins directory:
    mkdir -p /path/to/vault/.obsidian/plugins/vaultguard
    cp main.js manifest.json styles.css /path/to/vault/.obsidian/plugins/vaultguard/
    
  3. Enable the plugin in Obsidian Settings > Community Plugins

For End Users (Recommended Flow)

  1. Admin sends an invite email via the VaultGuard admin panel
  2. User receives email with:
    • A unique invite link
    • Instructions to install the plugin
    • The organization's configuration URL
  3. User installs the plugin (from community plugins or manual .zip)
  4. On first launch, user enters the configuration URL or invite code
  5. Plugin auto-configures the API endpoint and org ID
  6. User authenticates with their credentials (or SSO)

Building From Source

cd vaultguard-obsidian-plugin
npm install
npm run build

# Output: main.js, manifest.json, styles.css

Configuration

Plugin Settings

After installation, the plugin needs these settings (accessible via Settings > Obsidian VaultGuard):

Setting Description Example
API Endpoint Backend API URL https://api.vaultguard.cloud
Organization ID Your org identifier org_abc123def456
Sync Mode Real-time, periodic, or manual realtime
Encryption Client-side encryption toggle enabled
Cache Duration Local cache TTL (minutes) 60

Configuration via URL

Invite links can configure the plugin with a single URL:

obsidian://vaultguard-invite?org=acme-corp&email=user@example.com

For self-hosted deployments, include the API base URL:

obsidian://vaultguard-invite?org=acme-corp&email=user@example.com&api=https://api.example.com

Environment-Specific Config

For organizations with staging/production environments:

{
  "environments": {
    "production": {
      "apiEndpoint": "https://api.vaultguard.cloud",
      "orgId": "org_prod_abc123"
    },
    "staging": {
      "apiEndpoint": "https://api-staging.vaultguard.cloud",
      "orgId": "org_staging_xyz789"
    }
  }
}

Testing the Deployment

Automated Health Check

# API health
curl https://YOUR_API_ENDPOINT/health

# Expected response:
# {"status":"healthy","version":"1.0.0","services":{"dynamodb":"ok","s3":"ok","cognito":"ok"}}

End-to-End Test Sequence

# 1. Authenticate with Cognito through the plugin/admin panel or a trusted
# Cognito test harness, then open a VaultGuard server session.
ID_TOKEN="<Cognito ID token>"
SESSION_ID=$(curl -s -X POST https://YOUR_API_ENDPOINT/auth/session \
  -H "Authorization: $ID_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{}' | jq -r '.sessionId')

# 2. List users
curl -s https://YOUR_API_ENDPOINT/users \
  -H "Authorization: $ID_TOKEN" \
  -H "X-VaultGuard-Session-Id: $SESSION_ID" | jq .

# 3. List vaults you can see and pick a VAULT_ID
curl -s https://YOUR_API_ENDPOINT/vaults \
  -H "Authorization: $ID_TOKEN" \
  -H "X-VaultGuard-Session-Id: $SESSION_ID" | jq .
VAULT_ID="<vaultId from previous response>"

# 4. Upload a test file (vault-scoped — note the /vaults/{vaultId} prefix)
curl -X PUT https://YOUR_API_ENDPOINT/vaults/$VAULT_ID/files/test.md \
  -H "Authorization: $ID_TOKEN" \
  -H "X-VaultGuard-Session-Id: $SESSION_ID" \
  -H "Content-Type: application/json" \
  -d '{"content":"IyBUZXN0IEZpbGU=","contentType":"text/markdown"}'
# content is base64-encoded; this is "# Test File"

# 5. Verify permissions enforcement
curl -s https://YOUR_API_ENDPOINT/vaults/$VAULT_ID/files/restricted/secret.md \
  -H "Authorization: $ID_TOKEN" \
  -H "X-VaultGuard-Session-Id: $SESSION_ID"
# Should return 403 if user lacks permission, 404 if file does not exist

# 6. Check audit log
curl -s "https://YOUR_API_ENDPOINT/vaults/$VAULT_ID/audit?limit=5" \
  -H "Authorization: $ID_TOKEN" \
  -H "X-VaultGuard-Session-Id: $SESSION_ID" | jq .

Auth header note. The API Gateway Cognito authorizer expects the raw ID token, not Bearer <token>. The plugin sends the token with no prefix; do the same in your shell.

Plugin Verification

  1. Open Obsidian with the plugin enabled
  2. Check the status bar for connection indicator (green = connected)
  3. Open a file -- verify no permission errors in console
  4. Open the admin panel (command palette: "VaultGuard: Open Admin Panel")
  5. Verify users, permissions, and audit log load correctly

Monitoring and Alerts

CloudWatch Dashboard

The deployment creates a CloudWatch dashboard at: https://console.aws.amazon.com/cloudwatch/home?region=REGION#dashboards:name=VaultGuardDashboard

Key Metrics to Monitor

Metric Alarm Threshold Description
API 5xx Errors > 5 in 5 min Backend failures
API Latency p99 > 3000ms Performance degradation
Auth Failures > 20 in 5 min Potential brute force
DynamoDB Throttles > 0 Capacity issues
Lambda Errors > 3 in 5 min Function failures
S3 4xx Errors > 50 in 5 min Permission or path issues

Setting Up Alerts

# SNS topic for alerts (created by terraform/modules/monitoring, verify it exists)
aws sns list-topics | grep vaultguard

# Subscribe your email
aws sns subscribe \
  --topic-arn arn:aws:sns:REGION:ACCOUNT:VaultGuardAlerts \
  --protocol email \
  --notification-endpoint ops-team@company.com

# Subscribe a Slack webhook (via Lambda)
aws sns subscribe \
  --topic-arn arn:aws:sns:REGION:ACCOUNT:VaultGuardAlerts \
  --protocol https \
  --notification-endpoint https://hooks.slack.com/services/YOUR/WEBHOOK/URL

Log Groups

Log Group Content
/aws/lambda/vaultguard-api API request/response logs
/aws/lambda/vaultguard-auth Authentication events
/aws/lambda/vaultguard-sync File sync operations
vaultguard/audit Structured audit trail

Backup Strategy

DynamoDB (User Data, Permissions, Metadata)

  • Point-in-Time Recovery: Enabled by default (35-day window)
  • On-Demand Backups: Weekly via scheduled Lambda
# Manual backup
aws dynamodb create-backup \
  --table-name vaultguard-production-permissions \
  --backup-name "manual-$(date +%Y%m%d)"

S3 (Encrypted Vault Content)

  • Versioning: Enabled (retains all file versions)
  • Cross-Region Replication: Recommended for disaster recovery
  • Lifecycle Rules: Move old versions to Glacier after 90 days
# Enable cross-region replication (update bucket policy first)
aws s3api put-bucket-replication \
  --bucket vaultguard-vault-production-xxxxx \
  --replication-configuration file://replication-config.json

Cognito (User Accounts)

  • Cognito does not support direct export/backup
  • Mitigation: Sync user roster to DynamoDB via post-auth Lambda trigger
  • For disaster recovery: Store user list externally, re-create via admin API

Recovery Procedures

Scenario RTO RPO Procedure
Single file corruption < 5 min 0 (versioned) Restore from S3 version
DynamoDB table loss < 1 hour < 5 min PITR restore
Region failure < 4 hours < 15 min Failover to DR region
Account compromise < 1 hour 0 Rotate keys, revoke all, restore from backup

Cost Estimation

Estimated monthly costs by team size (USD, us-east-1 pricing, moderate usage):

Component 5 Users 10 Users 25 Users 50 Users 100 Users
Cognito $0 $0 $0 $0 $0
API Gateway $3 $7 $15 $30 $60
Lambda $1 $2 $5 $12 $25
DynamoDB $5 $8 $15 $30 $60
S3 Storage $2 $5 $12 $25 $50
S3 Requests $1 $2 $5 $10 $20
KMS $1 $1 $3 $5 $10
CloudWatch $3 $5 $8 $12 $20
Data Transfer $1 $2 $5 $10 $20
Total ~$17 ~$32 ~$68 ~$134 ~$265

Notes:

  • Cognito is free for the first 50,000 MAUs
  • Costs assume moderate sync frequency (every 5 minutes) and average file sizes of 10KB
  • S3 storage assumes 50MB per user average vault size
  • DynamoDB uses on-demand pricing; provisioned capacity is cheaper at scale
  • Actual costs vary based on sync frequency, file sizes, and API call patterns

Cost Optimization Tips:

  • Use DynamoDB provisioned capacity with auto-scaling above 25 users
  • Enable S3 Intelligent-Tiering for infrequently accessed files
  • Set appropriate CloudWatch log retention (7-14 days for non-audit logs)
  • Use Lambda ARM (Graviton) for ~20% cost reduction

Troubleshooting

Plugin Cannot Connect to API

Symptoms: Status bar shows "Offline", operations fail with network errors.

Checks:

  1. Verify API endpoint is correct in plugin settings
  2. Test endpoint directly: curl https://YOUR_ENDPOINT/health
  3. Check if corporate firewall/proxy blocks WebSocket or HTTPS to AWS
  4. Verify the API Gateway stage is deployed: AWS Console > API Gateway > Stages

Authentication Failures

Symptoms: "Invalid credentials" or "Session expired" errors.

Checks:

  1. Verify user exists in Cognito: aws cognito-idp admin-get-user --user-pool-id POOL_ID --username EMAIL
  2. Check if user is confirmed: UserStatus should be CONFIRMED
  3. Verify client ID matches: plugin config clientId == Cognito app client ID
  4. Check CloudWatch logs: /aws/lambda/vaultguard-auth
  5. If SSO: verify identity provider configuration and attribute mappings

Permission Denied Errors

Symptoms: 403 responses when accessing files the user should have access to.

Checks:

  1. Open admin panel > check user's effective permissions for the path
  2. Verify no explicit "None" (deny) rule overriding grants
  3. Check if permission has expired (look at expiry date)
  4. Check inheritance -- parent folder permissions propagate down
  5. Query directly: GET /vaults/{vaultId}/permissions/user/{userId} returns every rule applicable to that user inside the vault. To check a single action, POST /vaults/{vaultId}/permissions/check with { userId, action, path, roles } returns { allowed, matchedRule, evaluatedRules }.

Sync Conflicts

Symptoms: File content unexpectedly reverts or duplicates.

Checks:

  1. Check if multiple devices are syncing simultaneously for the same user
  2. Review file history: admin panel > audit log, filter by file path
  3. Verify system clocks are synchronized (NTP) on client devices
  4. If using periodic sync, check interval is not too aggressive (minimum 30s)

High Latency

Symptoms: Operations take > 3 seconds, UI feels sluggish.

Checks:

  1. Check CloudWatch API latency metrics
  2. Verify DynamoDB is not throttling: check ThrottledRequests metric
  3. Check Lambda cold starts: enable provisioned concurrency for critical functions
  4. Verify client region matches deployment region (latency from geographic distance)
  5. Check S3 transfer acceleration if users are far from the region

Terraform Deployment Failures

Symptoms: terraform apply fails with errors.

Common Fixes:

# "Resource already exists" -- import the conflicting resource into state
terraform import 'module.MODULE.RESOURCE.NAME' RESOURCE_ID

# "Role/Policy limit exceeded" -- request limit increase
aws service-quotas request-service-quota-increase ...

# "Certificate not in us-east-1" -- ACM certs for CloudFront must be in us-east-1.
# The dns module already pins the us_east_1 provider for the CloudFront cert.
# If you see this error, confirm the aws.us_east_1 provider alias in versions.tf.

# State lock stuck after an interrupted apply
terraform force-unlock LOCK_ID

Data Recovery

# Restore a DynamoDB table from point-in-time
aws dynamodb restore-table-to-point-in-time \
  --source-table-name vaultguard-production-permissions \
  --target-table-name vaultguard-production-permissions-restored \
  --restore-date-time "2024-01-15T10:30:00Z"

# Restore an S3 object version
aws s3api list-object-versions --bucket BUCKET --prefix "path/to/file"
aws s3api get-object --bucket BUCKET --key "path/to/file" --version-id "VERSION_ID" restored-file.md

Appendix: Architecture Reference

Client (Obsidian Plugin)
  |
  |-- HTTPS/WSS --> API Gateway
  |                    |
  |                    |--> Lambda (Auth)     --> Cognito User Pool
  |                    |--> Lambda (Files)    --> S3 (encrypted, versioned)
  |                    |--> Lambda (Perms)    --> DynamoDB (permissions table)
  |                    |--> Lambda (Audit)    --> DynamoDB (audit table)
  |                    |--> Lambda (Sync)     --> DynamoDB + S3
  |
  |-- KMS envelope encryption for all vault content
  |-- CloudWatch for monitoring/alerting
  |-- SNS for notification delivery

Next Steps

After deployment:

  1. Invite your first batch of users via the admin panel
  2. Set up folder-level permissions for your vault structure
  3. Configure monitoring alerts for your ops channel
  4. Run a security review using the Security Model doc
  5. Set up the backup verification schedule (monthly restore test)