Avoid resource sprawling using dynamic credential templating in Hashicorp Boundary
Background
- Is your organisation using Hashicorp Boundary as a PAM (privileged access management) solution for thousands of hosts residing in private network?
- Are you using Hashicorp Vault for generating dynamic secrets for multiple users and hosts?
- Have you faced a challenge of maintaining user specific targets and credential libraries in Boundary and eventually ended up in a resource sprawl?
In this article, I am going to highlight the solution using dynamic credential templating in Hashicorp Boundary which will help in avoiding resource sprawl.
Problem Statement
- In Boundary, a credential store is a resource that can retrieve, store, and potentially generate credentials (like Hashicorp Vault).
- Credential Store will contain credential libraries pointing to specific paths within Vault.
- These credential libraries are then mapped to Boundary targets which allows a Boundary user to connect to a host residing in private network. Now, if Vault is making use of a secret engine where we have defined user-specific roles like SSH-OTP (for linux servers) or LDAP (for Windows servers), in Boundary, we end up creating user-specific credential libraries pointing to user-specific Vault paths as shown below. This leads to resource sprawl within Boundary, resulting in hundreds to thousands of individual credential libraries at scale.
How do we solve resource sprawling?
In Boundary 0.12, support for credential templating within credential libraries was added. This allows Boundary administrators to configure one target with one credential library that generates per-user credentials. hence, you don’t need to maintain a target for each user as shown below. The paths in these credential libraries can be mapped to Boundary user’s or account’s information as highlighted here. The user’s/account’s information is dynamically populated while accessing credentials.
Code Snippet & Snapshots (Before vs After)
Before : Using Static Credential Libraries and Targets
# Static Linux Host Set
resource "boundary_host_set_static" "linux_servers_ssh" {
type = "static"
name = "linux_servers_ssh"
description = "Host set for linux servers"
host_catalog_id = boundary_host_catalog_static.linux_servers_catalog.id
host_ids = [for host in boundary_host_static.linux_servers_host : host.id]
}
# User specific credential libraries pointing to user-specific Vault paths.
resource "boundary_credential_library_vault" "cl_vault_ssh" {
name = "cl-ssh-${each.key}"
for_each = var.linux_users
credential_store_id = boundary_credential_store_vault.cs_vault.id
path = "ssh/creds/${each.key}"
http_method = "POST"
http_request_body = <<EOT
{
"ip": "1.2.3.4"
}
EOT
}
# 1:1 mapping between user-specific targets and user-specific credential libraries
resource "boundary_target" "linux_servers_target" {
for_each = var.linux_users
type = "tcp"
name = "linux-target-${each.key}"
description = "Linux Server Target"
scope_id = boundary_scope.project.id
default_port = "22"
host_source_ids = [
boundary_host_set_static.linux_servers_ssh.id
]
brokered_credential_source_ids = [
boundary_credential_library_vault.cl_vault_ssh["${each.key}"].id
]
}
After : Using dynamic credential libraries and targets
# Credential Store for Vault
resource "boundary_credential_store_vault" "cs_vault" {
name = "cs-pam-vault"
description = "Vault for PAM"
address = var.vault_address
token = var.vault_token
scope_id = boundary_scope.project.id
}
# Dynamic Credential Library for Vault SSH-OTP paths
resource "boundary_credential_library_vault" "cl_vault_dynamic_ssh" {
name = "cl-dynamic-ssh"
credential_store_id = boundary_credential_store_vault.cs_vault.id
path = "ssh/creds/{{ .User.Name }}"
http_method = "POST"
http_request_body = <<EOT
{
"ip": "1.2.3.4"
}
EOT
}
# Dynamic Host Set for every team
resource "boundary_host_set_plugin" "dynamic_host_set_team" {
name = "dynamic-host-set-${var.team}"
host_catalog_id = var.host_catalog_id
attributes_json = jsonencode({
"filter" = "tagName eq 'team' and tagValue eq '${var.team_tag}'",
})
}
# Dynamic Target for every team
resource "boundary_target" "dynamic_target_team" {
type = "tcp"
name = "dynamic-target-${var.team}"
description = "Dynamic Target for team - ${var.team}"
scope_id = var.project_id
default_port = 22
host_source_ids = [boundary_host_set_plugin.dynamic_host_set_team.id]
brokered_credential_source_ids = boundary_credential_library_vault.cl_vault_dynamic_ssh.id
}
Conclusion
Due to dynamic credential templating, you can very easily create managed groups in Boundary and assign team-specific targets mapped to dynamic host catalogs and single dynamic credential library path using user’s information as shown in the above code snippet.