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
- AWS Infrastructure Deployment
- Cognito Configuration
- Initial Admin Setup
- Plugin Installation
- Configuration
- Testing the Deployment
- Monitoring and Alerts
- Backup Strategy
- Cost Estimation
- Troubleshooting
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". Passingstage=prodhere 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
stageto make the name "honest." The production stack'sstagevariable isdevfor historical reasons but it is production (vaultguard.cloud). Almost every resource name is derived from${var.stage}, so aplan/applywith-var="stage=prod"would recreate and destroy the live vaults, users, subscriptions, and audit history (a ~117-resource destroy). The remote-state key is namedprodpurely for clarity; thestagevar staysdev. For THIS stack, every terraform command usesstage=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)
- Download the latest release from the releases page
- 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/ - Enable the plugin in Obsidian Settings > Community Plugins
For End Users (Recommended Flow)
- Admin sends an invite email via the VaultGuard admin panel
- User receives email with:
- A unique invite link
- Instructions to install the plugin
- The organization's configuration URL
- User installs the plugin (from community plugins or manual .zip)
- On first launch, user enters the configuration URL or invite code
- Plugin auto-configures the API endpoint and org ID
- 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
- Open Obsidian with the plugin enabled
- Check the status bar for connection indicator (green = connected)
- Open a file -- verify no permission errors in console
- Open the admin panel (command palette: "VaultGuard: Open Admin Panel")
- 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:
- Verify API endpoint is correct in plugin settings
- Test endpoint directly:
curl https://YOUR_ENDPOINT/health - Check if corporate firewall/proxy blocks WebSocket or HTTPS to AWS
- Verify the API Gateway stage is deployed: AWS Console > API Gateway > Stages
Authentication Failures
Symptoms: "Invalid credentials" or "Session expired" errors.
Checks:
- Verify user exists in Cognito:
aws cognito-idp admin-get-user --user-pool-id POOL_ID --username EMAIL - Check if user is confirmed:
UserStatusshould beCONFIRMED - Verify client ID matches: plugin config
clientId== Cognito app client ID - Check CloudWatch logs:
/aws/lambda/vaultguard-auth - 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:
- Open admin panel > check user's effective permissions for the path
- Verify no explicit "None" (deny) rule overriding grants
- Check if permission has expired (look at expiry date)
- Check inheritance -- parent folder permissions propagate down
- 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/checkwith{ userId, action, path, roles }returns{ allowed, matchedRule, evaluatedRules }.
Sync Conflicts
Symptoms: File content unexpectedly reverts or duplicates.
Checks:
- Check if multiple devices are syncing simultaneously for the same user
- Review file history: admin panel > audit log, filter by file path
- Verify system clocks are synchronized (NTP) on client devices
- If using periodic sync, check interval is not too aggressive (minimum 30s)
High Latency
Symptoms: Operations take > 3 seconds, UI feels sluggish.
Checks:
- Check CloudWatch API latency metrics
- Verify DynamoDB is not throttling: check
ThrottledRequestsmetric - Check Lambda cold starts: enable provisioned concurrency for critical functions
- Verify client region matches deployment region (latency from geographic distance)
- 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:
- Invite your first batch of users via the admin panel
- Set up folder-level permissions for your vault structure
- Configure monitoring alerts for your ops channel
- Run a security review using the Security Model doc
- Set up the backup verification schedule (monthly restore test)