Boundary + Vault: Zero-Trust Database Access with Credential Brokering

Your developers need database access. Traditional solutions to this problem create security debt:
Shared credentials in a wiki that everyone can see. SSH tunnels through bastion hosts that become single points of failure. VPN connections that grant far more network access than necessary. Service accounts with passwords that never get rotated.
Every one of these approaches leaves you exposed. And if you've ever tried to answer "who accessed the production database last month?" you know how painful the audit trail can be.
Boundary with Vault credential brokering offers a fundamentally different model.

Developers authenticate with their corporate identity. Boundary authorizes access to specific databases. Vault generates unique, short-lived credentials for each session. No shared passwords. No standing access. Complete audit trail.
Let me walk you through implementing this pattern on AWS.
_____
Architecture Overview
Here's the flow:
- Developer runs
boundary connect postgres -target-id=... - Boundary authenticates via Okta OIDC
- Boundary checks authorization - is this user allowed to access this target?
- Boundary requests dynamic credentials from Vault
- Vault generates a unique PostgreSQL user with limited privileges
- Boundary injects credentials and proxies the connection
- Session ends → credentials auto-expire
The developer never sees or handles database credentials. They authenticate once with their corporate identity, and everything else happens automatically.
_____
Infrastructure Setup with Terraform
Project Structure
1boundary-vault-rds/
2├── modules/
3│ ├── boundary/
4│ ├── vault/
5│ └── rds/
6├── environments/
7│ └── prod/
8│ ├── main.tf
9│ ├── variables.tf
10│ └── terraform.tfvars
11└── scripts/
12 └── configure-vault.shVPC and Networking
1# modules/networking/main.tf
2module "vpc" {
3 source = "terraform-aws-modules/vpc/aws"
4 version = "~> 5.0"
5
6 name = "${var.project_name}-${var.environment}"
7 cidr = var.vpc_cidr
8
9 azs = ["${var.region}a", "${var.region}b", "${var.region}c"]
10 private_subnets = [cidrsubnet(var.vpc_cidr, 4, 0), cidrsubnet(var.vpc_cidr, 4, 1), cidrsubnet(var.vpc_cidr, 4, 2)]
11 public_subnets = [cidrsubnet(var.vpc_cidr, 4, 3), cidrsubnet(var.vpc_cidr, 4, 4), cidrsubnet(var.vpc_cidr, 4, 5)]
12
13 enable_nat_gateway = true
14 single_nat_gateway = var.environment != "prod"
15 enable_dns_hostnames = true
16 enable_dns_support = true
17
18 tags = var.common_tags
19}RDS PostgreSQL
1# modules/rds/main.tf
2resource "aws_db_instance" "application" {
3 identifier = "${var.project_name}-${var.environment}"
4 engine = "postgres"
5 engine_version = "15.4"
6 instance_class = var.environment == "prod" ? "db.r6g.large" : "db.t3.medium"
7
8 allocated_storage = 100
9 max_allocated_storage = 500
10 storage_type = "gp3"
11 storage_encrypted = true
12 kms_key_id = aws_kms_key.rds.arn
13
14 db_name = "application"
15 username = "vault_admin"
16 password = random_password.rds_admin.result
17
18 vpc_security_group_ids = [aws_security_group.rds.id]
19 db_subnet_group_name = aws_db_subnet_group.main.name
20
21 backup_retention_period = 7
22 backup_window = "03:00-04:00"
23 maintenance_window = "Mon:04:00-Mon:05:00"
24
25 deletion_protection = var.environment == "prod"
26 skip_final_snapshot = var.environment != "prod"
27
28 tags = var.common_tags
29}
30
31resource "aws_security_group" "rds" {
32 name = "${var.project_name}-rds"
33 description = "Allow access from Boundary workers only"
34 vpc_id = var.vpc_id
35
36 # Only Boundary workers can reach the database
37 ingress {
38 description = "PostgreSQL from Boundary workers"
39 from_port = 5432
40 to_port = 5432
41 protocol = "tcp"
42 security_groups = [var.boundary_worker_sg_id]
43 }
44
45 tags = var.common_tags
46}Notice the security group - only Boundary workers can reach the database. Developers connect through Boundary, never directly.
______
Vault Configuration for Dynamic Credentials
This is where the magic happens. Vault's database secrets engine generates unique credentials on-demand.
Enable Database Secrets Engine
1# modules/vault/database.tf
2resource "vault_mount" "database" {
3 path = "database"
4 type = "database"
5 description = "Dynamic database credentials"
6}
7
8resource "vault_database_secret_backend_connection" "postgres" {
9 backend = vault_mount.database.path
10 name = "application-db"
11 allowed_roles = ["readonly", "readwrite", "admin"]
12
13 postgresql {
14 connection_url = "postgresql://{{username}}:{{password}}@${var.rds_endpoint}:5432/application"
15 username = var.rds_admin_username
16 password = var.rds_admin_password
17 }
18}
19
20# Read-only role for developers
21resource "vault_database_secret_backend_role" "readonly" {
22 backend = vault_mount.database.path
23 name = "readonly"
24 db_name = vault_database_secret_backend_connection.postgres.name
25
26 creation_statements = [
27 "CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';",
28 "GRANT CONNECT ON DATABASE application TO \"{{name}}\";",
29 "GRANT USAGE ON SCHEMA public TO \"{{name}}\";",
30 "GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";",
31 "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO \"{{name}}\";"
32 ]
33
34 revocation_statements = [
35 "REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA public FROM \"{{name}}\";",
36 "REVOKE USAGE ON SCHEMA public FROM \"{{name}}\";",
37 "DROP ROLE IF EXISTS \"{{name}}\";"
38 ]
39
40 default_ttl = 3600 # 1 hour
41 max_ttl = 14400 # 4 hours
42}
43
44# Read-write role for applications
45resource "vault_database_secret_backend_role" "readwrite" {
46 backend = vault_mount.database.path
47 name = "readwrite"
48 db_name = vault_database_secret_backend_connection.postgres.name
49
50 creation_statements = [
51 "CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';",
52 "GRANT CONNECT ON DATABASE application TO \"{{name}}\";",
53 "GRANT USAGE ON SCHEMA public TO \"{{name}}\";",
54 "GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO \"{{name}}\";",
55 "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO \"{{name}}\";"
56 ]
57
58 default_ttl = 1800 # 30 minutes
59 max_ttl = 3600 # 1 hour
60}Vault Policy for Boundary
1# modules/vault/policies.tf
2resource "vault_policy" "boundary_credential_store" {
3 name = "boundary-credential-store"
4
5 policy = <<-EOT
6 # Allow Boundary to request database credentials
7 path "database/creds/readonly" {
8 capabilities = ["read"]
9 }
10
11 path "database/creds/readwrite" {
12 capabilities = ["read"]
13 }
14
15 # Allow token renewal
16 path "auth/token/renew-self" {
17 capabilities = ["update"]
18 }
19
20 path "auth/token/lookup-self" {
21 capabilities = ["read"]
22 }
23 EOT
24}
25
26resource "vault_token" "boundary" {
27 policies = [vault_policy.boundary_credential_store.name]
28 renewable = true
29 ttl = "24h"
30 no_parent = true
31
32 metadata = {
33 purpose = "boundary-credential-store"
34 }
35}_____
Boundary Configuration
Scopes and Auth Method
1# modules/boundary/config.tf
2resource "boundary_scope" "global" {
3 global_scope = true
4 scope_id = "global"
5}
6
7resource "boundary_scope" "org" {
8 scope_id = boundary_scope.global.id
9 name = "engineering"
10 description = "Engineering organization"
11}
12
13resource "boundary_scope" "project" {
14 scope_id = boundary_scope.org.id
15 name = "databases"
16 description = "Database access project"
17}
18
19# OIDC Auth with Okta
20resource "boundary_auth_method_oidc" "okta" {
21 scope_id = boundary_scope.org.id
22 name = "Okta"
23 issuer = "https://${var.okta_domain}"
24 client_id = var.okta_client_id
25 client_secret = var.okta_client_secret
26 signing_algorithms = ["RS256"]
27 api_url_prefix = "https://${var.boundary_domain}"
28 claims_scopes = ["profile", "email", "groups"]
29 account_claim_maps = ["oid=sub", "name=name", "email=email"]
30}
31
32# Map Okta groups to Boundary roles
33resource "boundary_managed_group" "developers" {
34 auth_method_id = boundary_auth_method_oidc.okta.id
35 name = "developers"
36 filter = "\"developers\" in \"/token/groups\""
37}
38
39resource "boundary_managed_group" "dba" {
40 auth_method_id = boundary_auth_method_oidc.okta.id
41 name = "dba"
42 filter = "\"database-admins\" in \"/token/groups\""
43}Vault Credential Store
1# modules/boundary/credentials.tf
2resource "boundary_credential_store_vault" "main" {
3 scope_id = boundary_scope.project.id
4 name = "vault"
5 address = "https://${var.vault_domain}"
6 token = vault_token.boundary.client_token
7 namespace = var.vault_namespace
8}
9
10resource "boundary_credential_library_vault" "db_readonly" {
11 credential_store_id = boundary_credential_store_vault.main.id
12 name = "db-readonly"
13 path = "database/creds/readonly"
14 http_method = "GET"
15 credential_type = "username_password"
16}
17
18resource "boundary_credential_library_vault" "db_readwrite" {
19 credential_store_id = boundary_credential_store_vault.main.id
20 name = "db-readwrite"
21 path = "database/creds/readwrite"
22 http_method = "GET"
23 credential_type = "username_password"
24}Targets and Permissions
1# modules/boundary/targets.tf
2resource "boundary_host_catalog_static" "databases" {
3 scope_id = boundary_scope.project.id
4 name = "databases"
5}
6
7resource "boundary_host_static" "app_db" {
8 host_catalog_id = boundary_host_catalog_static.databases.id
9 name = "application-database"
10 address = var.rds_endpoint
11}
12
13resource "boundary_host_set_static" "app_db" {
14 host_catalog_id = boundary_host_catalog_static.databases.id
15 name = "application-database"
16 host_ids = [boundary_host_static.app_db.id]
17}
18
19# Read-only target for developers
20resource "boundary_target" "db_readonly" {
21 scope_id = boundary_scope.project.id
22 name = "app-db-readonly"
23 description = "Read-only access to application database"
24 type = "tcp"
25 default_port = 5432
26
27 host_source_ids = [boundary_host_set_static.app_db.id]
28
29 brokered_credential_source_ids = [
30 boundary_credential_library_vault.db_readonly.id
31 ]
32
33 worker_filter = "\"database-access\" in \"/tags/type\""
34
35 session_max_seconds = 14400 # 4 hours max session
36 session_connection_limit = 10
37}
38
39# Role granting developers read-only access
40resource "boundary_role" "developer_db_access" {
41 scope_id = boundary_scope.project.id
42 name = "developer-db-access"
43 principal_ids = [boundary_managed_group.developers.id]
44
45 grant_strings = [
46 "ids=${boundary_target.db_readonly.id};actions=authorize-session"
47 ]
48}
49
50# Role granting DBAs full access
51resource "boundary_role" "dba_db_access" {
52 scope_id = boundary_scope.project.id
53 name = "dba-db-access"
54 principal_ids = [boundary_managed_group.dba.id]
55
56 grant_strings = [
57 "ids=${boundary_target.db_readonly.id},${boundary_target.db_readwrite.id};actions=authorize-session"
58 ]
59}______
Developer Experience
Connecting to the Database
From the developer's perspective, it's simple:
1# Authenticate with Okta (opens browser)
2boundary authenticate oidc -auth-method-id amoidc_abc123
3
4# List available targets
5boundary targets list -scope-id p_xyz789
6
7# Connect - credentials injected automatically
8boundary connect postgres \
9 -target-id ttcp_readonly123 \
10 -dbname application
11
12# psql session opens with dynamic credentials
13# No password prompt - Boundary injected itWhat the Developer Sees
1$ boundary connect postgres -target-id ttcp_readonly123 -dbname application
2
3 Proxy listening on 127.0.0.1:52341
4 Credentials brokered:
5 Credential Source ID: clvlt_abc123
6 Username: v-token-readonly-xyz789
7 Password: <hidden>
8
9psql (15.4)
10Type "help" for help.
11
12application=> SELECT current_user;
13 current_user
14---------------------
15 v-token-readonly-xyz789
16(1 row)That username - v-token-readonly-xyz789 - is a Vault-generated credential. It exists only for this session and expires when the session ends or after the TTL, whichever comes first.
______
Audit and Compliance
Every connection generates audit events:
- Who authenticated
- What target they accessed
- When the session started and ended
- What credentials were brokered
1{
2 "timestamp": "2024-01-15T10:30:00Z",
3 "type": "session_authorization",
4 "user_id": "u_abc123",
5 "user_email": "developer@company.com",
6 "target_id": "ttcp_readonly123",
7 "target_name": "app-db-readonly",
8 "credential_source": "clvlt_abc123",
9 "credential_type": "username_password",
10 "worker_id": "w_xyz789"
11}When compliance asks "who accessed production data last quarter?", you have the answer.
______
Wrapping Up
Boundary with Vault credential brokering eliminates the messy reality of database access in most organizations. No more shared credentials in wikis. No more long-lived service accounts. No more "who has access to production?" uncertainty.
Developers get seamless access through their existing corporate identity. Security teams get granular control and full audit trails. DBAs stop fielding credential rotation requests.
The initial setup is complex - you're deploying two systems with tight integration. But the operational simplicity and security posture improvement justify the investment for any organization serious about data access governance.
This is what zero-trust access actually looks like in practice.
