Licensing in 2026

Boundary moved to BSL (Business Source License) along with the rest of HashiCorp's portfolio. The community edition is free for internal use. The fork to watch is OpenBao for Vault; Boundary doesn't have a popular fork yet. For homelab + small-team use, BSL is fine.

The architecture

  • Controller — the brain. Stores state (in a database; Postgres recommended); serves the API + web UI.
  • Worker — the proxy. Sits between user and target. Multiple workers can be deployed (one per network zone). Workers don't need credentials to targets at rest — they get them from the controller per session.
  • Clientboundary CLI or Desktop app; users authenticate and start sessions.
  • Vault (optional but recommended) — for dynamic credential issuance (see Vault tutorial). Boundary can request DB credentials, SSH certs, etc. valid only for the session's lifetime.

Install (single-host dev mode for testing)

# macOS / Linux
curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | \
    sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install boundary boundary-desktop

# Dev mode (in-memory; testing only)
boundary dev
# Prints admin password + URLs; the controller listens on :9200, worker on :9202

Production install

For production, controller + worker run as separate processes with HCL config:

# controller.hcl
disable_mlock = true

controller {
  name = "controller-1"
  description = "main controller"
  database {
    url = "postgresql://boundary:<pw>@db/boundary?sslmode=disable"
  }
  api_rate_limit_disable = false
}

listener "tcp" {
  address = "0.0.0.0"
  purpose = "api"
  tls_disable = false
  tls_cert_file = "/etc/boundary/server.crt"
  tls_key_file  = "/etc/boundary/server.key"
}

listener "tcp" {
  address = "0.0.0.0"
  purpose = "cluster"
  tls_disable = true     # internal cluster traffic; secure via network
}

kms "aead" {
  purpose   = "root"
  aead_type = "aes-gcm"
  key       = "<base64-32-bytes>"
  key_id    = "boundary-root"
}

kms "aead" {
  purpose   = "worker-auth"
  aead_type = "aes-gcm"
  key       = "<base64-32-bytes>"
  key_id    = "boundary-worker-auth"
}

kms "aead" {
  purpose   = "recovery"
  aead_type = "aes-gcm"
  key       = "<base64-32-bytes>"
  key_id    = "boundary-recovery"
}

Initialize the database + start:

boundary database init -config /etc/boundary/controller.hcl
boundary server -config /etc/boundary/controller.hcl

Worker config on a separate host (with reachable connectivity to controller + targets):

# worker.hcl
disable_mlock = true

listener "tcp" {
  purpose = "proxy"
  address = "0.0.0.0:9202"
}

worker {
  name = "worker-prod-us-east-1"
  description = "prod workers"
  initial_upstreams = ["controller.example.com:9201"]
  tags { type = ["prod", "us-east-1"] }
}

kms "aead" {
  purpose   = "worker-auth"
  aead_type = "aes-gcm"
  key       = "<same as controller's worker-auth key>"
  key_id    = "boundary-worker-auth"
}

Authenticate + create the first user

# Authenticate as the initial admin (created during database init)
boundary authenticate password \
    -auth-method-id ampw_xxx \
    -login-name admin \
    -password '<initial-pw>'

# Token saved locally; subsequent CLI calls use it

# Add an OIDC auth method (Authentik / Okta / Google)
boundary auth-methods create oidc \
    -name "Authentik" \
    -issuer "https://auth.example.com/application/o/boundary/" \
    -client-id "<client>" \
    -client-secret "<secret>" \
    -signing-algorithms "RS256" \
    -api-url-prefix "https://boundary.example.com"

Define targets

# Create a host catalog (where targets live)
boundary host-catalogs create static -name "prod-hosts" -scope-id global

# Add a host
boundary hosts create static \
    -host-catalog-id <catalog-id> \
    -address "10.0.5.10" \
    -name "db-prod-1"

# Group hosts
boundary host-sets create static \
    -host-catalog-id <catalog-id> \
    -name "prod-dbs"

# Add the host to the set
boundary host-sets add-hosts -id <set-id> -host <host-id>

# Create a target (the connectable endpoint)
boundary targets create tcp \
    -name "prod-postgres" \
    -default-port 5432 \
    -scope-id <project-id> \
    -host-source-ids <host-set-id>

Users connect to targets

# User CLI: list available targets
boundary targets list

# Start a session (Boundary proxies localhost → target)
boundary connect -target-id <target-id>
# Boundary opens local port <random> that proxies to the target.

# Or with helper formats
boundary connect ssh -target-id <target-id>     # spawns ssh with the right args
boundary connect postgres -target-id <target-id> # spawns psql

# The desktop app does this graphically

The user never gets the target's actual IP. Boundary's worker is the only one with that.

Dynamic credentials from Vault

The killer feature. Configure a Vault credential library on a target; Boundary requests a credential at session-start, injects it into the proxied connection, revokes it at session-end.

# Create a credential store backed by Vault
boundary credential-stores create vault \
    -name "vault-prod" \
    -address "https://vault.example.com:8200" \
    -token "<periodic-token>" \
    -scope-id <project>

# Vault must have a database secret engine configured for the target DB
# Boundary requests creds from this library at session-start
boundary credential-libraries create vault \
    -credential-store-id <store-id> \
    -name "prod-postgres-readonly" \
    -vault-path "database/creds/readonly-role" \
    -http-method GET

# Attach to the target
boundary targets add-credential-sources \
    -id <target-id> \
    -brokered-credential-source <library-id>

Now when a user connects to prod-postgres, Vault mints a short-lived DB user (e.g. 1-hour TTL); Boundary uses it for the session; Vault revokes the user after the session. No human ever sees the actual password.

Roles + grants

# RBAC: "DBAs can list + connect to prod databases"
boundary roles create -name dba -scope-id <org>
boundary roles add-grants \
    -id <role-id> \
    -grant "id=*;type=target;actions=list,read,authorize-session"
boundary roles add-principals \
    -id <role-id> \
    -principal <user-or-group-id>

Session recording

Boundary can record SSH sessions (Ent feature; community edition has the audit log without keystroke recording). Combined with a S3 storage bucket for recording archives, gives full forensic ability for "what did the on-call engineer do during last night's incident."

Boundary vs alternatives

  • Teleport — commercial; broader scope (Kubernetes API access, app proxying, machine ID). More features; commercial license model is stricter.
  • StrongDM — commercial SaaS; similar shape; per-seat pricing.
  • OpenZiti — open-source overlay network with similar principles.
  • Cloudflare Access — commercial; tightly Cloudflare-coupled.
  • Plain SSH bastion + Vault SSH CA — simpler; less feature-rich.

When Boundary is the right tool

  • Teams with 10+ engineers needing access to 10+ targets and growing.
  • Strong audit / compliance requirements (per-session attestation, who-did-what).
  • Already invested in Vault + the HashiCorp stack.

When it isn't

  • For 1-2 engineers + a homelab, a Tailscale tailnet + SSH is simpler.
  • For very-fluid devops where standing access to internal services is the norm, the session model adds friction.
  • For BSL-license-averse environments, pick Teleport, OpenZiti, or roll-your-own with Vault SSH CA.