見出し画像

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が表示されます。

Boundaryのログイン画面

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の秘密鍵の管理

次回以降、これらの課題に取り組んでいきます!

この記事が気に入ったらサポートをしてみませんか?