Deploy controllers using a Helm chart
The controller chart is intended for operator-managed Boundary control planes on Kubernetes. It can run multiple controller replicas and uses Helm hook Jobs for database initialization. It can also optionally use hook Jobs for database migration, database repair, and admin bootstrap.
You must provide any required external dependencies such as PostgreSQL, KMS access, Boundary Enterprise license data, and TLS material, if necessary.
Deploying the Boundary controller Helm chart gives you a running Boundary control plane backed by PostgreSQL, ready to accept worker registrations and API traffic. This page covers installing the controller chart only. To add data-plane capacity, refer to Deploy workers using a Helm chart.
Prerequisites
Before installing the controller Helm chart, make sure the following are available:
- A Kubernetes cluster running version 1.34 or later.
- Helm 3 or later configured with access to the target Kubernetes cluster.
- A reachable PostgreSQL 15 or later database for Boundary controller state.
- A valid Boundary controller HCL configuration.
- KMS (key management system) configuration for the
root,recovery, andworker-authpurposes. - A Boundary Enterprise license and database URL.
- An existing Kubernetes Secret containing
admin-usernameandadmin-passwordwhenbootstrapAdmin.enabled=true. - A Kubernetes TLS Secret containing
tls.crtandtls.keywhentls.disabled=false. - A ServiceAccount that the controller deployment and hook Jobs can use.
The controller chart deploys a stateless Boundary control plane. PostgreSQL stores the persistent controller state, not the controller pods.
Complete the following steps to deploy a controller using the Helm chart.
Add the HashiCorp Helm repository
Add the Helm repository and confirm the chart is visible:
$ helm repo add hashicorp https://helm.releases.hashicorp.com; \
helm repo update; \
helm search repo hashicorp/boundary-controller
Example output:
NAME CHART VERSION APP VERSION DESCRIPTION
hashicorp/boundary-controller 0.1.0 1.0.0 Official HashiCorp Boundary Controller Helm Chart
Create the controller Secret
The chart reads sensitive values from an existing Kubernetes Secret referenced by secretRefs.secretName. At a minimum, create a Secret with the database URL and Boundary Enterprise license. Include the bootstrap admin credentials when bootstrapAdmin.enabled=true.
Refer to the Kubernetes Secrets documentation for instructions on creating Secrets.
Set secretRefs.keys.* to match the key names you used in your Secret. If your controller.config uses migration_url = "env://BOUNDARY_PG_MIGRATION_URL", also include the migration URL key referenced by secretRefs.keys.migrationUrl.
Create the TLS Secret when TLS is enabled
If tls.disabled=false, create a Kubernetes TLS Secret before installation. Refer to the Kubernetes Secrets documentation for instructions. The Secret must contain tls.crt and tls.key entries, and its name must match tls.secretName (default: boundary-controller-tls).
When TLS is disabled, the chart does not mount this Secret.
Prepare the controller configuration
The chart reads the Boundary controller HCL from the controller.config key in your values file. Set this value before installing the chart.
For a base controller configuration, refer to the controller configuration guidance in the controller stanza reference.
A usable controller configuration includes:
- An API listener
- A cluster listener
- An operations listener
- A
controllerblock withlicenseanddatabase.url - A
public_cluster_addrso workers can reach the cluster listener - KMS stanzas for
root,recovery, andworker-auth
Refer to the following example controller configuration using AWS KMS:
disable_mlock = true
listener "tcp" {
address = "0.0.0.0:9200"
purpose = "api"
tls_disable = false
tls_cert_file = "/etc/boundary/tls/tls.crt"
tls_key_file = "/etc/boundary/tls/tls.key"
}
listener "tcp" {
address = "0.0.0.0:9201"
purpose = "cluster"
}
listener "tcp" {
address = "0.0.0.0:9203"
purpose = "ops"
tls_disable = false
tls_cert_file = "/etc/boundary/tls/tls.crt"
tls_key_file = "/etc/boundary/tls/tls.key"
}
controller {
name = "boundary-controller"
description = "Boundary controller running in Kubernetes"
public_cluster_addr = "boundary-controller-cluster.boundary.svc.cluster.local:9201"
license = "<your-license-here>"
database {
url = "postgres://<user>:<password>@<host>:5432/<dbname>?sslmode=require"
}
}
kms "aead" {
purpose = "root"
aead_type = "aes-gcm"
key = "sP1fnF5Xz85RrXyELHFeZg9Ad2qt4Z4bgNHVGtD6sFo="
key_id = "global_root"
}
kms "aead" {
purpose = "recovery"
aead_type = "aes-gcm"
key = "8fZBjCUfN0TzjEGLQldGY4+iE9AkOvCfjh7+p0GtRBQ="
key_id = "global_recovery"
}
kms "aead" {
purpose = "worker-auth"
aead_type = "aes-gcm"
key = "ATTK2pMjMTEJGlAkUJAePnFMlNLQaXBnhSqJHVlJeKQ="
key_id = "global_worker-auth"
}
events {
audit_enabled = true
sysevents_enabled = true
observations_enabled = true
sink "stderr" {
name = "all-events"
description = "All events sent to stderr"
event_types = ["*"]
format = "hclog-text"
}
}
List available chart versions
Use the following command to see any available published versions of the controller chart:
$ helm search repo hashicorp/boundary-controller --versions
Install a Boundary controller
Create a values file that contains controller.config and the Kubernetes overrides you need:
secretRefs:
secretName: boundary-controller-secrets
serviceAccount:
name: default
tls:
disabled: false
controller:
config: |
disable_mlock = true
listener "tcp" {
address = "0.0.0.0:9200"
purpose = "api"
tls_disable = false
tls_cert_file = "/etc/boundary/tls/tls.crt"
tls_key_file = "/etc/boundary/tls/tls.key"
}
listener "tcp" {
address = "0.0.0.0:9201"
purpose = "cluster"
}
listener "tcp" {
address = "0.0.0.0:9203"
purpose = "ops"
tls_disable = false
tls_cert_file = "/etc/boundary/tls/tls.crt"
tls_key_file = "/etc/boundary/tls/tls.key"
}
controller {
name = "boundary-controller"
description = "Boundary controller running in Kubernetes"
public_cluster_addr = "boundary-controller-cluster.boundary.svc.cluster.local:9201"
license = "<your-license-here>"
database {
url = "postgres://<user>:<password>@<host>:5432/<dbname>?sslmode=require"
}
}
kms "aead" {
purpose = "root"
aead_type = "aes-gcm"
key = "sP1fnF5Xz85RrXyELHFeZg9Ad2qt4Z4bgNHVGtD6sFo="
key_id = "global_root"
}
kms "aead" {
purpose = "recovery"
aead_type = "aes-gcm"
key = "8fZBjCUfN0TzjEGLQldGY4+iE9AkOvCfjh7+p0GtRBQ="
key_id = "global_recovery"
}
kms "aead" {
purpose = "worker-auth"
aead_type = "aes-gcm"
key = "ATTK2pMjMTEJGlAkUJAePnFMlNLQaXBnhSqJHVlJeKQ="
key_id = "global_worker-auth"
}
events {
audit_enabled = true
sysevents_enabled = true
observations_enabled = true
sink "stderr" {
name = "all-events"
description = "All events sent to stderr"
event_types = ["*"]
format = "hclog-text"
}
}
service:
api:
type: LoadBalancer
cluster:
type: ClusterIP
ops:
type: ClusterIP
Run the following command to install the release, initialize the database schema, and create the bootstrap admin:
$ helm install boundary-controller hashicorp/boundary-controller \
--version 0.1.0 \
--namespace boundary \
--create-namespace \
--values my-values.yaml \
--set database.init.enabled=true \
--set bootstrapAdmin.enabled=true \
--wait
Example output:
NAME: boundary-controller
LAST DEPLOYED: Tue Jul 22 00:00:00 2026
NAMESPACE: boundary
STATUS: deployed
REVISION: 1
Or, to install without database initialization and bootstrap admin creation:
$ helm install boundary-controller hashicorp/boundary-controller \
--version 0.1.0 \
--namespace boundary \
--create-namespace \
--values my-values.yaml \
--wait
Verify the deployment
Check the rendered resources after installation:
$ kubectl get deployment,pods,svc,jobs,pdb --namespace boundary; \
kubectl logs --namespace boundary deployment/boundary-controller
Example output:
NAME READY STATUS RESTARTS AGE
pod/boundary-controller-0 1/1 Running 0 2m
pod/boundary-controller-1 1/1 Running 0 2m
pod/boundary-controller-db-init-abcde 0/1 Completed 0 2m
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/boundary-controller-api LoadBalancer 10.0.0.10 203.0.113.5 9200:31000/TCP 2m
service/boundary-controller-cluster ClusterIP 10.0.0.11 9201/TCP 2m
service/boundary-controller-ops ClusterIP 10.0.0.12 9203/TCP 2m
Confirm that:
- The database initialization job completes successfully when
database.init.enabled=true. - The bootstrap admin job completes successfully when
bootstrapAdmin.enabled=true. - The controller pods become Ready.
- The API, cluster, and ops services match your intended exposure model.
- Boundary API requests succeed against the controller API listener.
- Workers can reach
public_cluster_addrand register successfully.
Upgrade the controller
To upgrade the controller deployment, run:
$ helm upgrade boundary-controller hashicorp/boundary-controller \
--version 0.1.0 \
--namespace boundary \
--values my-values.yaml \
--rollback-on-failure \
--wait
For database migration scenarios, refer to Controller Helm chart examples.
Uninstall the controller
To remove the controller Helm release and its associated Kubernetes resources, run:
$ helm uninstall boundary-controller --namespace boundary
Troubleshooting
Bootstrap admin Job times out
If the bootstrap admin Job times out while connecting to the controller API Service, confirm that the TLS certificate in the TLS Secret includes the required SAN (DNS:<fullname>-api, for example boundary-controller-api), as described in Create the TLS Secret when TLS is enabled. A missing or mismatched SAN causes the Job to fail certificate verification and time out.
Next steps
To deploy workers, refer to Deploy workers using a Helm chart.
More information
- To view supported Helm values so that you can configure or update controllers, refer to Controller values.
- To view example configurations, refer to Controller Helm chart examples.