HashiCorp BoundaryをTerraform+Ansibleで構築
皆様、はじめまして。XAION DATAでインフラエンジニアとして活動しております坂下と申します。
今回はインフラのお話としてHashiCorp Boundaryを取り上げさせていただきます。
Boundaryとは
HashiCorp社といえば、IaCツールのTerraform、シークレット管理ツールのVault、VM構築ツールのVagrantなどが有名ですが、Boundaryはサーバへのリモートアクセスを実現するためのツールです。
従来はVPNや踏み台(Bastion)サーバを経由して会社のサーバへアクセスすることが一般的でしたが、Boundaryはこれらの代わりとなる機能を提供します。
Open ID ConnectによるSSOが可能
IdPにユーザを追加すればサーバへのアクセス権限を付与でき、削除すればサーバへのアクセス権限を取り消せます
ロールを用いて接続先サーバを制御可能
ユーザをロールに紐付けることで、接続可能なサーバを制限できます
サーバの動的検出
Dynamic host catalogs機能で、AWS EC2のインスタンスを自動的に接続先サーバとして登録できます (今回の記事では対象外です)
Terraformで設定を管理可能
HashiCorp社製品なのでTerraform対応はバッチリです
サーバに接続するためのクレデンシャルをBoundaryが代理挿入するCredential injectionという主要機能もありますが、こちらはEnterprise版もしくはHCP(SaaS)版のみの機能となるため、今回は割愛します。
(ユーザにクレデンシャルを渡す必要がなくなるため、よりセキュアな環境を実現できます)
なお、本記事を公開した2024年6月26日時点のBoundaryバージョンはv0.16.2です。まだv1.0.0を迎えていませんので、今後仕様が大きく変更となる可能性がありますのでご注意ください。
環境の構築
構成
AWS上にBoundaryを利用できる環境をTerraform+Ansibleで構築します。
BoundaryのサーバコンポーネントはControllerとWorkerから構成されますが、今回は同じEC2インスタンスでControllerとWorkerを稼働させます。
Controllerは認証・リソース・権限などの管理機能を、Workerはクライアントから宛先サーバ(Target Server)への通信のプロキシ機能を担っています。
クライアントとWorkerは直接通信する必要があるため、Boundary ServerをPublic subnetに配置してALBを経由しない経路も用意しています。
NLBを経由させることも可能ですが、ControllerがWorkerの死活監視やロードバランシングを実施しているため、今回は検証しておりません。
その他、以下の用途で各リソースを生成します。
ALB
Controllerへの通信(Web UI/API)のロードバランシング
ControllerのEC2インスタンス障害時の切り離し
ACM
ALBで利用するSSL証明書の発行
KMS
Boundaryが取り扱うデータの暗号化
RDS / PostgreSQL
Boundaryが取り扱うデータの保管
IAM Role
EC2インスタンスからKMSへアクセスするためのインスタンスプロファイル用のロール
AWS環境を構築するTerraform
AWSの各種リソースをTerraformで作成していきます。
VPC/サブネット/ALBは既に作成済のものを流用するため、dataで定義しておきます。
data "aws_vpc" "vpc" {
id = "vpc-XXXXXXXX"
}
# ap-northeast-1aのPublic subnet
data "aws_subnet" "apne1a" {
id = "subnet-XXXXXXXX"
}
# ap-northeast-1dのPublic subnet
data "aws_subnet" "apne1d" {
id = "subnet-XXXXXXXX"
}
data "aws_lb_listener" "https-listener" {
arn = "<ALBのリスナーのARN>"
}
# ALBに関連付けられているセキュリティグループ
data "aws_security_group" "alb" {
id = "sg-XXXXXXXXXXXXXXXXX"
}
まずはEC2インスタンスの定義です。2AZ分を作成するため、Local Valuesで定義してからfor_eachで必要なリソースを作成します。
locals {
boundary_instances = {
"apne1a" = {
az = "ap-northeast-1a",
subnet_id = data.aws_subnet.apne1a.id
}
"apne1d" = {
az = "ap-northeast-1d",
subnet_id = data.aws_subnet.apne1d.id
}
}
}
resource "aws_instance" "boundary" {
for_each = local.boundary_instances
# Amazon Linux 2023 AMI 2023.4.20240611.0 arm64 HVM kernel-6.1
ami = "ami-09ff6f432d0ee628e"
instance_type = "t4g.small"
vpc_security_group_ids = [
aws_security_group.boundary.id,
aws_security_group.boundary-cluster.id
]
key_name = "example_key"
iam_instance_profile = aws_iam_instance_profile.boundary.id
subnet_id = each.value.subnet_id
disable_api_termination = true
ebs_optimized = true
ebs_block_device {
device_name = "/dev/xvda"
volume_type = "gp3"
volume_size = 10
}
tags = {
Name = "Boundary Server (${each.value.az})"
}
volume_tags = {
Name = "Boundary Server (${each.value.az})"
}
}
次にALBの定義です。WebUI/APIサーバ(Controller)は9200/tcpでLISTENしていますが、ヘルスチェック用のエンドポイントは9203/tcpで用意されているのでhealth_checkはそちらを指定します。
参考 : https://developer.hashicorp.com/boundary/docs/operations/health
host_headerのvaluesは任意の値に変更してください。
resource "aws_lb_target_group" "boundary" {
name = "boundary"
target_type = "instance"
port = 9200
protocol = "HTTPS"
vpc_id = data.aws_vpc.vpc.id
health_check {
port = 9203
protocol = "HTTP"
path = "/health"
matcher = "200"
}
}
resource "aws_lb_target_group_attachment" "boundary" {
for_each = local.boundary_instances
target_group_arn = aws_lb_target_group.boundary.arn
target_id = aws_instance.boundary[each.key].id
}
resource "aws_lb_listener_rule" "boundary" {
listener_arn = data.aws_lb_listener.https-listener.arn
action {
type = "forward"
target_group_arn = aws_lb_target_group.boundary.arn
}
condition {
host_header {
values = ["boundary.example.com"]
}
}
}
次にセキュリティグループの定義です。9201/tcpはControllerがクラスタを組むために使用するポートです。Terraformではリソースの中に自分自身のリソースを記載することができないので、aws_security_group.boundary-clusterを別途作成してsecurity_groupsに指定しています。
9202/tcpはWorkerのポートのため、クライアントからの通信を直接受ける必要があります。そのため、cidr_blocksを0.0.0.0/0に設定して任意のIPアドレスからの通信を受け入れます。
SSHのcidr_blocksは任意の値に変更してください。
resource "aws_security_group" "boundary-cluster" {
name = "Boundary Cluster Security Group"
vpc_id = data.aws_vpc.vpc.id
}
resource "aws_security_group" "boundary" {
name = "Boundary Security Group"
vpc_id = data.aws_vpc.vpc.id
ingress {
description = "SSH"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["xxx.xxx.xxx.xxx/xx"]
}
ingress {
description = "Boundary api"
from_port = 9200
to_port = 9200
protocol = "tcp"
security_groups = [data.aws_security_group.alb.id]
}
ingress {
description = "Boundary cluster"
from_port = 9201
to_port = 9201
protocol = "tcp"
security_groups = [aws_security_group.boundary-cluster.id]
}
ingress {
description = "Boundary worker"
from_port = 9202
to_port = 9202
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "Boundary ops"
from_port = 9203
to_port = 9203
protocol = "tcp"
security_groups = [data.aws_security_group.alb.id]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
次にBoundaryが使用するKMSを定義します。各KMSの使用目的は以下で説明されています。
https://developer.hashicorp.com/boundary/docs/concepts/security/data-encryption
resource "aws_kms_key" "root" {
description = "Boundary root key"
deletion_window_in_days = 10
}
resource "aws_kms_key" "worker_auth" {
description = "Boundary worker authentication key"
deletion_window_in_days = 10
}
resource "aws_kms_key" "recovery" {
description = "Boundary recovery key"
deletion_window_in_days = 10
}
Boundary ServerのEC2インスタンスからKMSを操作するためのIAMロール/ポリシー/インスタンスプロファイルを定義します。
data "aws_iam_policy_document" "ec2-assume-role" {
statement {
actions = ["sts:AssumeRole"]
effect = "Allow"
principals {
type = "Service"
identifiers = ["ec2.amazonaws.com"]
}
}
}
resource "aws_iam_role" "boundary" {
name = "Boundary-Role"
assume_role_policy = data.aws_iam_policy_document.ec2-assume-role.json
}
data "aws_iam_policy_document" "boundary" {
statement {
actions = [
"kms:DescribeKey",
"kms:GenerateDataKey",
"kms:Decrypt",
"kms:Encrypt",
"kms:ListKeys",
"kms:ListAliases"
]
effect = "Allow"
resources = [
aws_kms_key.root.arn,
aws_kms_key.worker_auth.arn,
aws_kms_key.recovery.arn
]
}
}
resource "aws_iam_role_policy" "boundary" {
name = "Boundary-Policy"
role = aws_iam_role.boundary.id
policy = data.aws_iam_policy_document.boundary.json
}
resource "aws_iam_instance_profile" "boundary" {
name = "Boundary-InstanceProfile"
role = aws_iam_role.boundary.name
}
最後に、一般的なAWS Providerを使用するための定義を追加してTerraformを実行することで、Boundaryを動作させるためのAWSの各リソースが作成されます。
$ terraform init
$ terraform plan
$ terraform apply
BoundaryをインストールするAnsible Playbook
AWSの各種リソースが生成できたら、次はBoundaryをセットアップするためのAnsible Playbookを作成しましょう。Rolesを使用してディレクトリ分けをしています。
Boundaryの設定ファイルに記載する必要があるため、PostgreSQLのユーザとデータベースを事前に作成しておいてください。
./roles/boundary/tasks/main.yml
以下はタスクの内容です。
---
# HashiCorpの公式リポジトリを追加
- get_url:
url: https://rpm.releases.hashicorp.com/AmazonLinux/hashicorp.repo
dest: /etc/yum.repos.d/hashicorp.repo
owner: root
group: root
mode: '0644'
# Boundaryをインストール
- dnf:
name: boundary
state: present
notify: Restart boundary service
# 後述するパブリックIPv4アドレスを出力するディレクトリを作成
- file:
path: /run/boundary
state: directory
owner: boundary
group: boundary
mode: "0755"
# /runディレクトリは再起動で消えるため、起動時に/run/boundaryを自動生成するためのファイルを作成
- copy:
src: boundary.conf
dest: /etc/tmpfiles.d/boundary.conf
owner: root
group: root
mode: '0644'
# Service Unitをカスタマイズするためのディレクトリを作成
- file:
path: /etc/systemd/system/boundary.service.d
state: directory
owner: root
group: root
mode: '0755'
# Service Unitをカスタマイズするためのファイルを作成
- copy:
src: override.conf
dest: /etc/systemd/system/boundary.service.d/override.conf
owner: root
group: root
mode: '0644'
notify: Restart boundary service
# Boundaryの設定ファイルをテンプレートから作成
- template:
src: boundary.hcl.j2
dest: /etc/boundary.d/boundary.hcl
owner: boundary
group: boundary
mode: '0640'
notify: Restart boundary service
# BoundaryでTLS通信するための秘密鍵を作成
- community.crypto.openssl_privatekey:
path: /etc/pki/tls/private/boundary.key
size: 2048
owner: boundary
group: boundary
notify: Restart boundary service
# BoundaryでTLS通信するための自己署名証明書を作成
- community.crypto.x509_certificate:
path: /etc/pki/tls/certs/boundary.crt
privatekey_path: /etc/pki/tls/private/boundary.key
provider: selfsigned
owner: boundary
group: boundary
notify: Restart boundary service
./roles/boundary/handlers/main.yml
タスクで定義していたnotifyですが、Boundaryサービスの自動起動有効化と再起動をしています。
---
- name: Restart boundary service
systemd:
name: boundary.service
state: restarted
daemon_reload: yes
enabled: yes
./roles/boundary/vars/main.yml
変数の定義ファイルになります。
PostgreSQLのパスワードは必要に応じてansible-vault encrypt_stringなどで暗号化しておきましょう。
---
psql_password: "<PostgreSQL Password>"
root_kms_key_id: "<rootのKMSのキーID>"
worker_auth_kms_key_id: "<worker_authのKMSのキーID>"
recovery_kms_key_id: "<recoveryのKMSのキーID>"
./roles/boundary/files/boundary.conf
/run/boundaryディレクトリをOS起動時に自動生成するためのファイルです。
/runはtmpfsとなっているため、一度/run/boundaryディレクトリを作成しても再起動時に削除されます。この設定を/etc/tmpfiles.d/に置くことで、OS起動時にディレクトリが自動生成されるようになります。
d /run/boundary 0755 boundary boundary
./roles/boundary/files/override.conf
Boundaryの設定ファイルではWorkerのパブリックIPv4アドレスを指定する必要があります。そのため、Boundaryサービスの起動時にパブリックIPv4アドレスを/run/boundary/public-ipv4に出力するようにService Unitをカスタマイズしています。
ec2-metadataコマンドを実行するにはネットワークの疎通性が必要なので、After=network-online.targetを指定しています。
[Unit]
After=network-online.target
[Service]
ExecStartPre=/bin/sh -c '/usr/bin/ec2-metadata --public-ipv4 | /usr/bin/cut -d " " -f 2 > /run/boundary/public-ipv4'
./roles/boundary/templates/boundary.hcl.j2
Boundaryの設定ファイルのテンプレートファイルです。以下にいくつか設定を示します。url
PostgreSQLの接続情報を入力します
initial_upstreams
今回はControllerとWorkerが同居しているため、自インスタンスのプライベートIPv4アドレス(ansible_default_ipv4.address)を入力します
public_addr
クライアントが直接Workerへ通信するために、Worker自身のパブリックIPv4アドレスを入力します
前述したoverride.confで出力したパブリックIPv4アドレスのファイルを指定します
tls_cert_file / tls_key_file
タスクで生成した自己署名証明書を利用して、ALB/Controller間をHTTPS通信にします
ALBのターゲットは自己署名証明書でも通信可能です
disable_mlock = true
telemetry {
prometheus_retention_time = "24h"
disable_hostname = true
}
controller {
name = "controller_{{ ansible_board_asset_tag }}"
description = "{{ ansible_nodename }}"
database {
url = "postgresql://boundary:{{ psql_password }}@psql.example.com/boundary"
}
}
worker {
name = "worker_{{ ansible_board_asset_tag }}"
description = "{{ ansible_nodename }}"
initial_upstreams = [
"{{ ansible_default_ipv4.address }}"
]
public_addr = "file:///run/boundary/public-ipv4"
}
listener "tcp" {
purpose = "api"
address = "{{ ansible_default_ipv4.address }}"
tls_cert_file = "/etc/pki/tls/certs/boundary.crt"
tls_key_file = "/etc/pki/tls/private/boundary.key"
tls_min_version = "tls12"
}
listener "tcp" {
purpose = "cluster"
address = "{{ ansible_default_ipv4.address }}"
}
listener "tcp" {
purpose = "proxy"
address = "{{ ansible_default_ipv4.address }}"
}
listener "tcp" {
purpose = "ops"
address = "{{ ansible_default_ipv4.address }}"
tls_disable = true
}
kms "awskms" {
purpose = "root"
kms_key_id = "{{ root_kms_key_id }}"
}
kms "awskms" {
purpose = "worker-auth"
kms_key_id = "{{ worker_auth_kms_key_id }}"
}
kms "awskms" {
purpose = "recovery"
kms_key_id = "{{ recovery_kms_key_id }}"
}
これでAnsible Playbookの準備はほぼ完了です。後は一般的なansible.cfgやインベントリファイル、site.ymlなどを作成して、ansible-playbookを実行しましょう!
Boundaryの起動
Playbookの最後にBoundaryサービスの起動を行っていますが、データベースの中身が空の状態のため、起動に失敗します。
Boundary ServerのEC2インスタンスにSSHして、データベースの初期化を行います。今回はTerraformで管理したいので、リソースが自動生成されないようにskipの引数を付与しています。
$ sudo boundary database init \
-skip-auth-method-creation \
-skip-host-resources-creation \
-skip-initial-authenticated-user-role-creation \
-skip-initial-login-role-creation \
-skip-scopes-creation \
-skip-target-creation \
-config /etc/boundary.d/boundary.hcl
データベースの初期化が完了したら、Boundaryサービスを起動します。
問題なくBoundaryサービスが起動したら、もう一台のEC2インスタンスでもBoundaryサービスを忘れずに起動しましょう。
$ sudo systemctl start boundary.service
Boundaryを設定するTerraform
ようやくここまでやってきました。次は稼働中のBoundaryに対して設定を流し込むためのTerraformのファイルを作成します。
まずはTerraformとBoundary Providerの定義です。
addrにはALBで指定したドメインを入力します。
初期状態ではBoundaryにログインするためのクレデンシャル情報が無いため、recovery_kms_hclでリカバリKMSを指定します。
指定したKMSの鍵を取得してTerraformが実行されるため、実行環境にはKMSの鍵を取得するための権限が必要となります。
terraform {
backend "<任意のバックエンドをどうぞ>" {
...
}
required_providers {
boundary = {
source = "hashicorp/boundary"
}
}
}
provider "boundary" {
addr = "https://boundary.example.com"
recovery_kms_hcl = <<EOT
kms "awskms" {
purpose = "recovery"
kms_key_id = "<recoveryのKMSのキーID>"
}
EOT
}
Boundaryの階層構造は、Global → Organization → Project → Target となっています。
boundary_scopeではGlobal / Organization / Projectの設定が可能ですが、scope_idにGlobalを指定すると自動的にOrganization、Organizationを指定すると自動的にProjectとなります。
resource "boundary_scope" "global" {
scope_id = "global"
name = "global"
description = "Global Scope"
global_scope = true
}
resource "boundary_scope" "org" {
scope_id = boundary_scope.global.id
name = "Organization"
}
resource "boundary_scope" "project" {
scope_id = boundary_scope.org.id
name = "Project"
}
認証方式とアカウントの定義です。
パスワード認証とOIDC認証の設定を行っており、今回はOIDC認証のIdPにはGoogle Workspaceを使用します。
パスワード認証にはTerraformを実行するためのadminアカウントを作成します。後ほどWeb UIでログインするため、仮パスワードを入力します。
resource "boundary_auth_method_password" "password" {
scope_id = boundary_scope.global.id
name = "Password"
}
resource "boundary_account_password" "admin" {
auth_method_id = boundary_auth_method_password.password.id
login_name = "admin"
password = "<仮パスワード>"
}
resource "boundary_auth_method_oidc" "google" {
scope_id = boundary_scope.global.id
name = "Google"
issuer = "https://accounts.google.com"
client_id = "<クライアントID>"
client_secret = "<クライアントシークレット>"
signing_algorithms = ["RS256"]
claims_scopes = ["email", "profile"]
api_url_prefix = "https://boundary.example.com"
state = "active-public"
is_primary_for_scope = true
}
ユーザの定義です。
データベースの初期化をしたときにskip引数を付与していましたが、anonymous / authenticated / recoveryの3ユーザだけはデフォルトで存在します。後ほどanonymousユーザを使用するため、Data Sourceで定義しておきます。
OIDC認証についてはログイン時に自動的にユーザが作成されるため、設定は不要です。
data "boundary_user" "anonymous" {
name = "anonymous"
}
resource "boundary_user" "admin" {
scope_id = boundary_scope.global.id
name = "admin"
account_ids = [boundary_account_password.admin.id]
}
グループの定義です。
ロールの定義にて各グループを使用します。今回はどちらもメンバーの追加と削除はTerraformの管理外としたいため、ignore_changesでmember_idsを指定しておきます。
resource "boundary_group" "admin" {
scope_id = boundary_scope.global.id
name = "Administrator Group"
lifecycle {
ignore_changes = [member_ids]
}
}
resource "boundary_group" "developer" {
scope_id = boundary_scope.global.id
name = "dev@example.com"
lifecycle {
ignore_changes = [member_ids]
}
}
ロールの定義です。
Global / Organization / Project に対してそれぞれロールを作成します。(上の階層のロールが自動的に下の階層に適用されるようなことはありません)
ざっと以下のような権限付与を実施しています。
adminユーザとAdministratorグループ
各階層に対して全ての許可権限を付与
dev@example.comグループ
Global / Organization階層
下の階層のList(一覧表示)/Read(情報取得)権限のみ付与
Project階層
既存セッションのList/Read/Cancel(切断)権限
TargetのList権限
Targetにセッションを張る権限
anonymous (未ログインユーザを含む全てのリクエスト)
下の階層(Organization)のList権限
Organizationで認証方式の設定をすることも可能なため、未ログインユーザも取得する必要がある
認証方式のList/Authenticate(ログイン操作)権限
ログイン中アカウントのRead/Change-Password(パスワード変更)権限
認証トークンのList/Read/Delete(失効)権限
resource "boundary_role" "global-default" {
scope_id = boundary_scope.global.id
name = "Login and Default Grants"
principal_ids = [data.boundary_user.anonymous.id]
grant_strings = [
"ids=*;type=scope;actions=list,no-op",
"ids=*;type=auth-method;actions=authenticate,list",
"ids={{.Account.Id}};actions=read,change-password",
"ids=*;type=auth-token;actions=list,read:self,delete:self"
]
}
resource "boundary_role" "global-admin" {
scope_id = boundary_scope.global.id
name = "Administration"
principal_ids = [boundary_user.admin.id, boundary_group.admin.id]
grant_strings = [
"ids=*;type=*;actions=*"
]
}
resource "boundary_role" "global-developer" {
scope_id = boundary_scope.global.id
name = "Developer"
principal_ids = [boundary_group.developer.id]
grant_strings = [
"ids=*;type=scope;actions=list,read"
]
}
resource "boundary_role" "org-admin" {
scope_id = boundary_scope.org.id
name = "Administration"
principal_ids = [boundary_user.admin.id, boundary_group.admin.id]
grant_strings = [
"ids=*;type=*;actions=*"
]
}
resource "boundary_role" "org-developer" {
scope_id = boundary_scope.org.id
name = "Developer"
principal_ids = [boundary_group.developer.id]
grant_strings = [
"ids=*;type=scope;actions=list,read"
]
}
resource "boundary_role" "project-admin" {
scope_id = boundary_scope.project.id
name = "Administration"
principal_ids = [boundary_user.admin.id, boundary_group.admin.id]
grant_strings = [
"ids=*;type=*;actions=*"
]
}
resource "boundary_role" "project-developer" {
scope_id = boundary_scope.project.id
name = "Developer"
principal_ids = [boundary_group.developer.id]
grant_strings = [
"ids=*;type=session;actions=list,read:self,cancel:self",
"type=target;actions=list",
"ids=*;type=target;actions=authorize-session"
]
}
ターゲットの定義です。
複数のターゲットを定義した場合に、resourceを使い回せるようにLocal Valuesを使用しています。serversはEC2インスタンスへのSSH接続を、databasesは任意のデータベースへの接続を想定しています。
SSH接続の場合、宛先ポートは22/tcp固定で、クライアントポート(クライアント端末でLISTENするポート)は未指定(ランダム)です。クライアントポートはランダムでもProxyCommandで対応可能なため、この形式を採用しています。
一方、データベースへの接続は、アプリケーションによってLISTENポートが異なることや、クライアントのデータベース管理ツールからのアクセスが想定されるため、宛先ポートとクライアントポートの両方を指定する形式にしています。
(クライアントポートが変わると、都度データベース管理ツールの宛先ポートを変更する必要があります)
locals {
servers = {
"Target Server" = "server.example.com"
}
databases = {
"Target Database" = {
address = "database.example.com",
dest_port = 5432, client_port = 54321
}
}
}
resource "boundary_target" "servers" {
for_each = local.servers
scope_id = boundary_scope.project.id
type = "tcp"
name = each.key
default_port = 22
session_max_seconds = 43200
address = each.value
}
resource "boundary_target" "databases" {
for_each = local.databases
scope_id = boundary_scope.project.id
type = "tcp"
name = each.key
default_port = each.value.dest_port
default_client_port = each.value.client_port
session_max_seconds = 43200
address = each.value.address
}
さて、ようやくTerraformの定義が完了しました。
terraform init → terraform plan → 問題なければterraform applyを実行しましょう!
適用が完了したら、boundary_account_password.adminに入力していた仮パスワードの行は削除しても問題ありません。
ALBで指定したドメインにブラウザでアクセスすると、BoundaryのWeb UIが表示されます。
adminと仮パスワードでSign Inし、仮パスワードを別のパスワードに変更します。
(仮パスワードはtfstateファイルに残ってしまいます)
データベースへ接続する
Boundaryを利用して宛先サーバへ接続できる環境が整ったので、クライアント端末から宛先サーバ上のデータベースへ接続してみましょう。
Boundaryのクライアントをインストールします。macOSのHomebrew利用者であれば、以下のコマンドでインストールできます。
環境変数BOUNDARY_ADDRでBoundaryのドメインを指定します。
$ brew install hashicorp/tap/boundary
$ export BOUNDARY_ADDR=https://boundary.example.com
boundary auth-methods listを実行すると、利用可能な認証方式の一覧が表示されます。まずはパスワード認証を試したいので、ID: ampw_HYfZMMcNd9
の認証方式を利用します。
$ boundary auth-methods list
Auth Method information:
ID: amoidc_h4Vh5GN92p
Type: oidc
Name: Google
Is Primary For Scope: true
Authorized Actions:
authenticate
ID: ampw_HYfZMMcNd9
Type: password
Name: Password
Authorized Actions:
authenticate
boundary authenticate password -auth-method-id ampw_HYfZMMcNd9を実行すると、ログイン名とパスワードを要求されます。それぞれ入力するとBoundaryを利用するための認証トークンが取得できます。
$ boundary authenticate password -auth-method-id ampw_HYfZMMcNd9
Please enter the login name (it will be hidden):
Please enter the password (it will be hidden):
Authentication information:
Account ID: acctpw_lpjmswq6Wl
Auth Method ID: ampw_HYfZMMcNd9
Expiration Time: Sun, 30 Jun 2024 23:46:09 JST
User ID: u_Kyb6yEHh2T
The token name "default" was successfully stored in the chosen keyring and is not displayed here.
boundary targets list -recursiveを実行すると、boundary_targetで指定した宛先サーバの一覧が表示されます。今回はデータベースに接続したいので、ttcp_y520MhAtZ8を利用します。
$ boundary targets list -recursive
Target information:
ID: ttcp_y520MhAtZ8
Scope ID: p_3nqp4MAHNV
Version: 1
Type: tcp
Name: Target Database
Address: database.example.com
Authorized Actions:
remove-credential-sources
read
update
remove-host-sources
add-credential-sources
set-credential-sources
delete
set-host-sources
add-host-sources
no-op
authorize-session
ID: ttcp_ydR33hKmnE
Scope ID: p_3nqp4MAHNV
Version: 1
Type: tcp
Name: Target Server
Address: server.example.com
Authorized Actions:
delete
add-host-sources
add-credential-sources
set-credential-sources
read
authorize-session
no-op
update
remove-host-sources
set-host-sources
remove-credential-sources
boundary connect ttcp_y520MhAtZ8を実行すると、宛先サーバの5432/tcpポートとのトンネルが張られます。
$ boundary connect ttcp_y520MhAtZ8
Proxy listening information:
Address: 127.0.0.1
Connection Limit: 0
Expiration: Mon, 24 Jun 2024 11:48:38 JST
Port: 54321
Protocol: tcp
Session ID: s_Ev96j7tQyc
boundary connectの結果から、Boundaryが127.0.0.1の54321/tcpポートでLISTENしていることが分かります。
別のCLIセッションを開き、データベースに接続してみましょう。
無事にログインすることができました🎉
$ psql -h 127.0.0.1 -p 54321 -U postgres
Password for user postgres:
psql (16.3, server 15.2)
SSL connection (protocol: TLSv1.2, cipher: AES128-SHA256, compression: off)
Type "help" for help.
postgres=>
少し前に戻りますが、recoveryの権限は強すぎるので、TerraformのBoundary Providerの定義をパスワード認証に書き換えておきましょう。
(adminも強力ですが…)
provider "boundary" {
addr = "https://boundary.example.com"
auth_method_id = "ampw_HYfZMMcNd9"
auth_method_login_name = "admin"
auth_method_password = "<パスワード>"
}
おわりに
今回は長くなってしまいましたので、ここで一旦終わりにさせていただきます。Boundaryを使用して宛先サーバへの通信ができる環境を構築しましたが、まだいくつかの課題が残っています。
OIDC認証に関する課題
Google WorkspaceをIdPとして利用した場合、グループ情報を取得できないため、適切な権限付与ができない
そもそも、現在の設定ではOIDC認証でログインしてもユーザが作成されるだけで、実際に操作を行うことができない
adminユーザでログインし、手動でdev@example.comグループにユーザを追加する方法はあります
SSHの秘密鍵の管理
Boundaryはトンネルを張るだけであり、SSH接続時の秘密鍵の管理について考慮が必要です続きの記事を公開しました
次回以降、これらの課題に取り組んでいきます!