From a2757538d104cb700866d04d7fe78ca7dfd9716f Mon Sep 17 00:00:00 2001 From: Joshua Gilman Date: Tue, 18 Aug 2026 17:39:41 -0700 Subject: [PATCH] chore(aws): retire migrated infrastructure roots Remove legacy writers after the dedicated AWS repository verified all six roots with zero-change plans. --- .moon/workspace.yml | 4 - README.md | 11 +- aws/github-token-broker/.terraform.lock.hcl | 38 -- aws/github-token-broker/Justfile | 29 -- aws/github-token-broker/README.md | 50 -- aws/github-token-broker/backend.tf | 8 - aws/github-token-broker/data.tf | 14 - aws/github-token-broker/iam.tf | 66 --- aws/github-token-broker/lambda.tf | 47 -- aws/github-token-broker/locals.tf | 133 ----- aws/github-token-broker/moon.yml | 25 - aws/github-token-broker/outputs.tf | 34 -- aws/github-token-broker/providers.tf | 10 - aws/github-token-broker/scripts/check.sh | 11 - aws/github-token-broker/terraform.tf | 14 - aws/github-token-broker/tests/main.tftest.hcl | 155 ------ aws/github-token-broker/variables.tf | 146 ------ aws/keycloak/.gitignore | 10 - aws/keycloak/.terraform.lock.hcl | 38 -- aws/keycloak/Justfile | 29 -- aws/keycloak/README.md | 139 ----- aws/keycloak/backend.tf | 8 - aws/keycloak/compute.tf | 44 -- aws/keycloak/data.tf | 154 ------ aws/keycloak/github_token_broker.tf | 39 -- aws/keycloak/iam.tf | 30 -- aws/keycloak/locals.tf | 250 --------- aws/keycloak/moon.yml | 26 - aws/keycloak/network.tf | 59 --- aws/keycloak/outputs.tf | 80 --- aws/keycloak/providers.tf | 10 - aws/keycloak/scripts/check.sh | 11 - .../templates/keycloak/lab-realm.json.tftpl | 190 ------- .../templates/scripts/create-network.sh.tftpl | 6 - .../scripts/disable-bootstrap-admin.sh.tftpl | 50 -- .../templates/scripts/prepare-data.sh.tftpl | 31 -- .../scripts/run-keycloak-config.sh.tftpl | 80 --- .../templates/scripts/run-keycloak.sh.tftpl | 35 -- .../templates/scripts/run-postgres.sh.tftpl | 17 - .../templates/scripts/run-traefik.sh.tftpl | 24 - .../templates/scripts/wait-postgres.sh.tftpl | 14 - .../glab-keycloak-bootstrap.service.tftpl | 17 - .../glab-keycloak-config.service.tftpl | 11 - .../systemd/glab-keycloak-data.service.tftpl | 11 - ...loak-disable-bootstrap-admin.service.tftpl | 9 - .../glab-keycloak-network.service.tftpl | 13 - .../glab-keycloak-postgres.service.tftpl | 16 - .../glab-keycloak-traefik.service.tftpl | 15 - .../systemd/glab-keycloak.service.tftpl | 17 - .../templates/traefik_dynamic.yml.tftpl | 15 - aws/keycloak/terraform.tf | 10 - aws/keycloak/terraform.tfvars | 6 - aws/keycloak/tests/main.tftest.hcl | 485 ------------------ aws/keycloak/variables.tf | 467 ----------------- aws/lab-foundation/.gitignore | 10 - aws/lab-foundation/.terraform.lock.hcl | 20 - aws/lab-foundation/Justfile | 29 -- aws/lab-foundation/README.md | 42 -- aws/lab-foundation/backend.tf | 8 - aws/lab-foundation/locals.tf | 9 - aws/lab-foundation/main.tf | 90 ---- aws/lab-foundation/moon.yml | 25 - aws/lab-foundation/outputs.tf | 64 --- aws/lab-foundation/providers.tf | 10 - aws/lab-foundation/scripts/check.sh | 11 - aws/lab-foundation/terraform.tf | 10 - aws/lab-foundation/tests/main.tftest.hcl | 97 ---- aws/lab-foundation/variables.tf | 83 --- aws/subnet-router/.gitignore | 10 - aws/subnet-router/.terraform.lock.hcl | 20 - aws/subnet-router/Justfile | 29 -- aws/subnet-router/README.md | 40 -- aws/subnet-router/backend.tf | 8 - aws/subnet-router/compute.tf | 58 --- aws/subnet-router/data.tf | 78 --- aws/subnet-router/deployment.tf | 16 - aws/subnet-router/iam.tf | 30 -- aws/subnet-router/locals.tf | 27 - aws/subnet-router/moon.yml | 25 - aws/subnet-router/network.tf | 41 -- aws/subnet-router/outputs.tf | 29 -- aws/subnet-router/providers.tf | 10 - aws/subnet-router/scripts/check.sh | 11 - .../templates/dns_mirror.service.tftpl | 15 - .../templates/dns_mirror_bootstrap.sh.tftpl | 37 -- .../templates/dns_mirror_compose.yml.tftpl | 13 - .../templates/user_data.sh.tftpl | 31 -- aws/subnet-router/terraform.tf | 10 - aws/subnet-router/terraform.tfvars | 4 - aws/subnet-router/tests/main.tftest.hcl | 128 ----- aws/subnet-router/variables.tf | 137 ----- network/tailscale/.gitignore | 4 - network/tailscale/.terraform.lock.hcl | 24 - network/tailscale/Justfile | 41 -- network/tailscale/README.md | 21 - network/tailscale/backend.tf | 9 - network/tailscale/main.tf | 49 -- network/tailscale/outputs.tf | 9 - network/tailscale/providers.tf | 16 - network/tailscale/terraform.tfvars | 2 - network/tailscale/variables.tf | 37 -- security/pki/root-ca/.gitignore | 10 - security/pki/root-ca/.terraform.lock.hcl | 20 - security/pki/root-ca/Justfile | 36 -- security/pki/root-ca/README.md | 86 ---- security/pki/root-ca/backend.tf | 8 - security/pki/root-ca/main.tf | 22 - security/pki/root-ca/outputs.tf | 19 - security/pki/root-ca/providers.tf | 22 - security/pki/root-ca/root_ca.crt | 12 - security/pki/root-ca/root_ca.fingerprint | 1 - security/pki/root-ca/templates/root-ca.tpl | 9 - security/pki/root-ca/terraform.tfvars | 1 - security/pki/root-ca/variables.tf | 34 -- 114 files changed, 7 insertions(+), 5061 deletions(-) delete mode 100644 aws/github-token-broker/.terraform.lock.hcl delete mode 100644 aws/github-token-broker/Justfile delete mode 100644 aws/github-token-broker/README.md delete mode 100644 aws/github-token-broker/backend.tf delete mode 100644 aws/github-token-broker/data.tf delete mode 100644 aws/github-token-broker/iam.tf delete mode 100644 aws/github-token-broker/lambda.tf delete mode 100644 aws/github-token-broker/locals.tf delete mode 100644 aws/github-token-broker/moon.yml delete mode 100644 aws/github-token-broker/outputs.tf delete mode 100644 aws/github-token-broker/providers.tf delete mode 100755 aws/github-token-broker/scripts/check.sh delete mode 100644 aws/github-token-broker/terraform.tf delete mode 100644 aws/github-token-broker/tests/main.tftest.hcl delete mode 100644 aws/github-token-broker/variables.tf delete mode 100644 aws/keycloak/.gitignore delete mode 100644 aws/keycloak/.terraform.lock.hcl delete mode 100644 aws/keycloak/Justfile delete mode 100644 aws/keycloak/README.md delete mode 100644 aws/keycloak/backend.tf delete mode 100644 aws/keycloak/compute.tf delete mode 100644 aws/keycloak/data.tf delete mode 100644 aws/keycloak/github_token_broker.tf delete mode 100644 aws/keycloak/iam.tf delete mode 100644 aws/keycloak/locals.tf delete mode 100644 aws/keycloak/moon.yml delete mode 100644 aws/keycloak/network.tf delete mode 100644 aws/keycloak/outputs.tf delete mode 100644 aws/keycloak/providers.tf delete mode 100755 aws/keycloak/scripts/check.sh delete mode 100644 aws/keycloak/templates/keycloak/lab-realm.json.tftpl delete mode 100644 aws/keycloak/templates/scripts/create-network.sh.tftpl delete mode 100644 aws/keycloak/templates/scripts/disable-bootstrap-admin.sh.tftpl delete mode 100644 aws/keycloak/templates/scripts/prepare-data.sh.tftpl delete mode 100644 aws/keycloak/templates/scripts/run-keycloak-config.sh.tftpl delete mode 100644 aws/keycloak/templates/scripts/run-keycloak.sh.tftpl delete mode 100644 aws/keycloak/templates/scripts/run-postgres.sh.tftpl delete mode 100644 aws/keycloak/templates/scripts/run-traefik.sh.tftpl delete mode 100644 aws/keycloak/templates/scripts/wait-postgres.sh.tftpl delete mode 100644 aws/keycloak/templates/systemd/glab-keycloak-bootstrap.service.tftpl delete mode 100644 aws/keycloak/templates/systemd/glab-keycloak-config.service.tftpl delete mode 100644 aws/keycloak/templates/systemd/glab-keycloak-data.service.tftpl delete mode 100644 aws/keycloak/templates/systemd/glab-keycloak-disable-bootstrap-admin.service.tftpl delete mode 100644 aws/keycloak/templates/systemd/glab-keycloak-network.service.tftpl delete mode 100644 aws/keycloak/templates/systemd/glab-keycloak-postgres.service.tftpl delete mode 100644 aws/keycloak/templates/systemd/glab-keycloak-traefik.service.tftpl delete mode 100644 aws/keycloak/templates/systemd/glab-keycloak.service.tftpl delete mode 100644 aws/keycloak/templates/traefik_dynamic.yml.tftpl delete mode 100644 aws/keycloak/terraform.tf delete mode 100644 aws/keycloak/terraform.tfvars delete mode 100644 aws/keycloak/tests/main.tftest.hcl delete mode 100644 aws/keycloak/variables.tf delete mode 100644 aws/lab-foundation/.gitignore delete mode 100644 aws/lab-foundation/.terraform.lock.hcl delete mode 100644 aws/lab-foundation/Justfile delete mode 100644 aws/lab-foundation/README.md delete mode 100644 aws/lab-foundation/backend.tf delete mode 100644 aws/lab-foundation/locals.tf delete mode 100644 aws/lab-foundation/main.tf delete mode 100644 aws/lab-foundation/moon.yml delete mode 100644 aws/lab-foundation/outputs.tf delete mode 100644 aws/lab-foundation/providers.tf delete mode 100755 aws/lab-foundation/scripts/check.sh delete mode 100644 aws/lab-foundation/terraform.tf delete mode 100644 aws/lab-foundation/tests/main.tftest.hcl delete mode 100644 aws/lab-foundation/variables.tf delete mode 100644 aws/subnet-router/.gitignore delete mode 100644 aws/subnet-router/.terraform.lock.hcl delete mode 100644 aws/subnet-router/Justfile delete mode 100644 aws/subnet-router/README.md delete mode 100644 aws/subnet-router/backend.tf delete mode 100644 aws/subnet-router/compute.tf delete mode 100644 aws/subnet-router/data.tf delete mode 100644 aws/subnet-router/deployment.tf delete mode 100644 aws/subnet-router/iam.tf delete mode 100644 aws/subnet-router/locals.tf delete mode 100644 aws/subnet-router/moon.yml delete mode 100644 aws/subnet-router/network.tf delete mode 100644 aws/subnet-router/outputs.tf delete mode 100644 aws/subnet-router/providers.tf delete mode 100755 aws/subnet-router/scripts/check.sh delete mode 100644 aws/subnet-router/templates/dns_mirror.service.tftpl delete mode 100644 aws/subnet-router/templates/dns_mirror_bootstrap.sh.tftpl delete mode 100644 aws/subnet-router/templates/dns_mirror_compose.yml.tftpl delete mode 100644 aws/subnet-router/templates/user_data.sh.tftpl delete mode 100644 aws/subnet-router/terraform.tf delete mode 100644 aws/subnet-router/terraform.tfvars delete mode 100644 aws/subnet-router/tests/main.tftest.hcl delete mode 100644 aws/subnet-router/variables.tf delete mode 100644 network/tailscale/.gitignore delete mode 100644 network/tailscale/.terraform.lock.hcl delete mode 100644 network/tailscale/Justfile delete mode 100644 network/tailscale/README.md delete mode 100644 network/tailscale/backend.tf delete mode 100644 network/tailscale/main.tf delete mode 100644 network/tailscale/outputs.tf delete mode 100644 network/tailscale/providers.tf delete mode 100644 network/tailscale/terraform.tfvars delete mode 100644 network/tailscale/variables.tf delete mode 100644 security/pki/root-ca/.gitignore delete mode 100644 security/pki/root-ca/.terraform.lock.hcl delete mode 100644 security/pki/root-ca/Justfile delete mode 100644 security/pki/root-ca/README.md delete mode 100644 security/pki/root-ca/backend.tf delete mode 100644 security/pki/root-ca/main.tf delete mode 100644 security/pki/root-ca/outputs.tf delete mode 100644 security/pki/root-ca/providers.tf delete mode 100644 security/pki/root-ca/root_ca.crt delete mode 100644 security/pki/root-ca/root_ca.fingerprint delete mode 100644 security/pki/root-ca/templates/root-ca.tpl delete mode 100644 security/pki/root-ca/terraform.tfvars delete mode 100644 security/pki/root-ca/variables.tf diff --git a/.moon/workspace.yml b/.moon/workspace.yml index 047dba4..515e6f7 100644 --- a/.moon/workspace.yml +++ b/.moon/workspace.yml @@ -1,9 +1,5 @@ projects: sources: - aws-github-token-broker: 'aws/github-token-broker' - aws-keycloak: 'aws/keycloak' - aws-lab-foundation: 'aws/lab-foundation' - aws-subnet-router: 'aws/subnet-router' compute-incusos: 'compute/incusos' root: '.' network-vyos: 'network/vyos' diff --git a/README.md b/README.md index fadb32d..a074dfd 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,13 @@ The first active project is `network/vyos`, which holds the VyOS gateway configuration and validation flow. Additional infrastructure domains can be added as separate Moon projects without reworking the repository baseline. +## AWS Infrastructure Migration + +AWS and Tailscale OpenTofu roots moved to the private +[`GilmanLab/aws`](https://github.com/GilmanLab/aws) repository on 2026-08-19. +This repository is no longer an infrastructure writer for those state objects. +Do not restore or apply the removed roots from Git history. + ## Quick Start Prerequisites: @@ -29,13 +36,9 @@ moon run network-vyos:check ## Current Projects -- `aws/lab-foundation`: OpenTofu for the base VPC, DNS, and KMS primitives in the lab AWS account -- `aws/subnet-router`: OpenTofu for the AWS EC2 subnet router that joins Tailscale using AWS workload identity federation - `compute/incusos`: IncusOS host bootstrap inputs; currently pivoting from the UM760/Tinkerbell path to the N5 Pro genesis node -- `network/tailscale`: Tailscale DNS settings managed via OpenTofu - `network/vyos`: VyOS gateway automation, config, and static validation -- `security/pki/root-ca`: OpenTofu for the offline-by-policy root CA KMS key ## Support diff --git a/aws/github-token-broker/.terraform.lock.hcl b/aws/github-token-broker/.terraform.lock.hcl deleted file mode 100644 index 6628ee4..0000000 --- a/aws/github-token-broker/.terraform.lock.hcl +++ /dev/null @@ -1,38 +0,0 @@ -# This file is maintained automatically by "tofu init". -# Manual edits may be lost in future updates. - -provider "registry.opentofu.org/hashicorp/archive" { - version = "2.7.1" - constraints = "~> 2.7" - hashes = [ - "h1:/Y6fLmEGMtbcAFi3ALu5tAwEIfUc8vGZRErNjMIfi2U=", - "zh:4f8fe5f92125fc7be91379dbde004aaf676fbb523082af167d0a57ac723836bc", - "zh:4fba9a08c254fd3c17464c1e13398e4927b1d3e22bfdc3bb66c4e5bd9573ada4", - "zh:65e9945c1e89333b01ef25c15518e125817268f9ecddc3f9d5337dc120d342ee", - "zh:6cc92ec02475310612a2fc663ab22366c17005203be55287e9af316ac0397ef4", - "zh:7e9efa56a27ea28c7a19465b4223f43653988639123160b09507cbc7a9ad5458", - "zh:9c77863b5ff47196cec4e82ce9b943c8a0de5840f7b1f82c7e96d92d8be7c7c1", - "zh:b6498ff9e2e717c94e5d2c494a2071acc123e8195bfdd9f7965a0676fa866b06", - "zh:c5941326ffe88ff77d15fb1212f746ea57eebf7556bc2707c3a054ad0e5a6ab0", - "zh:ec1c14feeeb3b78be2ab37533c6ef2e0d6417c6faa82ddd62c446db77f618926", - "zh:ed4643c4f8d9f7d060c01463317d68315b6af7197beaba331451ca3991a9c990", - ] -} - -provider "registry.opentofu.org/hashicorp/aws" { - version = "5.100.0" - constraints = "~> 5.90" - hashes = [ - "h1:BrNG7eFOdRrRRbHdvrTjMJ8X8Oh/tiegURiKf7J2db8=", - "zh:1a41f3ee26720fee7a9a0a361890632a1701b5dc1cf5355dc651ddbe115682ff", - "zh:30457f36690c19307921885cc5e72b9dbeba369445815903acd5c39ac0e41e7a", - "zh:42c22674d5f23f6309eaf3ac3a4f1f8b66b566c1efe1dcb0dd2fb30c17ce1f78", - "zh:4cc271c795ff8ce6479ec2d11a8ba65a0a9ed6331def6693f4b9dccb6e662838", - "zh:60932aa376bb8c87cd1971240063d9d38ba6a55502c867fdbb9f5361dc93d003", - "zh:864e42784bde77b18393ebfcc0104cea9123da5f4392e8a059789e296952eefa", - "zh:9750423138bb01ecaa5cec1a6691664f7783d301fb1628d3b64a231b6b564e0e", - "zh:e5d30c4dec271ef9d6fe09f48237ec6cfea1036848f835b4e47f274b48bda5a7", - "zh:e62bd314ae97b43d782e0841b13e68a3f8ec85cc762004f973ce5ce7b6cdbfd0", - "zh:ea851a3c072528a4445ac6236ba2ce58ffc99ec466019b0bd0e4adde63a248e4", - ] -} diff --git a/aws/github-token-broker/Justfile b/aws/github-token-broker/Justfile deleted file mode 100644 index 4894a50..0000000 --- a/aws/github-token-broker/Justfile +++ /dev/null @@ -1,29 +0,0 @@ -set shell := ["bash", "-euo", "pipefail", "-c"] - -default: - @just --list - -# Offline validation: no backend or AWS credentials required. -check: - ./scripts/check.sh - -# Format Tofu files in place. -fmt: - tofu fmt -recursive - -# Initialize the working directory against the S3 backend. -init: - test -n "${GLAB_AWS_STATE_BUCKET:-}" || { echo "Set GLAB_AWS_STATE_BUCKET to the pre-created S3 backend bucket." >&2; exit 1; } - tofu init -reconfigure -backend-config="bucket=${GLAB_AWS_STATE_BUCKET}" - -# Render and save a plan to `tfplan`. -plan: - tofu plan -out=tfplan - -# Apply the saved plan produced by `just plan`. -apply: - tofu apply tfplan - -# Show outputs from the last applied state. -output: - tofu output diff --git a/aws/github-token-broker/README.md b/aws/github-token-broker/README.md deleted file mode 100644 index 4203e08..0000000 --- a/aws/github-token-broker/README.md +++ /dev/null @@ -1,50 +0,0 @@ -# aws/github-token-broker - -> Deprecated: the GitHub token broker is now owned by `aws/keycloak` through -> the reusable `meigma/github-token-broker` Terraform module. Keep this stack -> only long enough to destroy the legacy `glab-github-token-broker` resources -> in AWS before applying `aws/keycloak`. The legacy state may also own the lab -> GitHub Actions OIDC provider; do not run a full destroy unless you have -> confirmed that provider is not shared. - -OpenTofu stack for the GitHub token broker Lambda in the `lab` account. - -This stack creates: - -- a Go custom-runtime Lambda using `provided.al2023` and `arm64` -- a Lambda execution role scoped to the GitHub App bootstrap SSM parameters -- a CloudWatch log group with explicit retention -- a GitHub Actions OIDC provider, unless an existing provider ARN is supplied -- a tag-scoped publisher role for `GilmanLab/platform` release workflows -- an invoke policy that future bootstrap principals can attach - -Terraform owns the Lambda infrastructure. The `platform` release workflow owns -function code updates after the initial placeholder package creates the -function. - -## Prerequisites - -- OpenTofu `>= 1.10` -- `just` -- `AWS_PROFILE` set to the `lab` account admin profile -- `GLAB_AWS_STATE_BUCKET` set to the pre-created S3 backend bucket in the - `lab` account -- the GitHub App SSM parameters created in session 027: - `/glab/bootstrap/github-app/client-id`, - `/glab/bootstrap/github-app/installation-id`, and - `/glab/bootstrap/github-app/private-key-pem` - -If the lab account already has a GitHub Actions OIDC provider, set -`github_oidc_provider_arn` and import/reuse it rather than creating a duplicate. - -## Usage - -```sh -just check -just init -just plan -just apply -``` - -After the first apply, release `platform/services/github-token-broker` so the -`github-token-broker-v*` tag workflow publishes the real Lambda package. diff --git a/aws/github-token-broker/backend.tf b/aws/github-token-broker/backend.tf deleted file mode 100644 index 0afe4a3..0000000 --- a/aws/github-token-broker/backend.tf +++ /dev/null @@ -1,8 +0,0 @@ -terraform { - backend "s3" { - key = "aws/github-token-broker.tfstate" - region = "us-west-2" - encrypt = true - use_lockfile = true - } -} diff --git a/aws/github-token-broker/data.tf b/aws/github-token-broker/data.tf deleted file mode 100644 index ac0438b..0000000 --- a/aws/github-token-broker/data.tf +++ /dev/null @@ -1,14 +0,0 @@ -data "aws_caller_identity" "current" {} - -data "archive_file" "placeholder" { - output_path = "${path.module}/.terraform/github-token-broker-placeholder.zip" - type = "zip" - - source { - content = <<-EOT - #!/bin/sh - echo "github-token-broker placeholder" - EOT - filename = "bootstrap" - } -} diff --git a/aws/github-token-broker/iam.tf b/aws/github-token-broker/iam.tf deleted file mode 100644 index 3b5e8db..0000000 --- a/aws/github-token-broker/iam.tf +++ /dev/null @@ -1,66 +0,0 @@ -resource "aws_iam_openid_connect_provider" "github_actions" { - count = var.github_oidc_provider_arn == "" ? 1 : 0 - - client_id_list = [ - var.github_oidc_audience, - ] - url = var.github_oidc_provider_url - - tags = merge(local.common_tags, { - Name = "github-actions" - }) -} - -resource "aws_iam_role" "execution" { - assume_role_policy = local.lambda_assume_role_policy - name = var.execution_role_name - - tags = merge(local.common_tags, { - Name = var.execution_role_name - }) -} - -resource "aws_iam_role_policy" "execution_logs" { - name = "${var.execution_role_name}-logs" - policy = local.lambda_logs_policy - role = aws_iam_role.execution.id -} - -resource "aws_iam_role_policy" "execution_ssm" { - name = "${var.execution_role_name}-ssm" - policy = local.lambda_ssm_policy - role = aws_iam_role.execution.id -} - -resource "aws_iam_role_policy" "execution_private_key_kms" { - count = var.private_key_kms_key_arn == "" ? 0 : 1 - - name = "${var.execution_role_name}-private-key-kms" - policy = local.lambda_private_key_kms_policy - role = aws_iam_role.execution.id -} - -resource "aws_iam_role" "publisher" { - assume_role_policy = local.publisher_assume_role_policy - name = var.publisher_role_name - - tags = merge(local.common_tags, { - Name = var.publisher_role_name - }) -} - -resource "aws_iam_role_policy" "publisher" { - name = "${var.publisher_role_name}-lambda-code" - policy = local.publisher_policy - role = aws_iam_role.publisher.id -} - -resource "aws_iam_policy" "invoke" { - description = "Allows bootstrap principals to invoke the GitHub token broker Lambda." - name = "${var.function_name}-invoke" - policy = local.invoke_policy - - tags = merge(local.common_tags, { - Name = "${var.function_name}-invoke" - }) -} diff --git a/aws/github-token-broker/lambda.tf b/aws/github-token-broker/lambda.tf deleted file mode 100644 index 335af0b..0000000 --- a/aws/github-token-broker/lambda.tf +++ /dev/null @@ -1,47 +0,0 @@ -resource "aws_cloudwatch_log_group" "broker" { - name = "/aws/lambda/${var.function_name}" - retention_in_days = var.log_retention_in_days - - tags = merge(local.common_tags, { - Name = "/aws/lambda/${var.function_name}" - }) -} - -resource "aws_lambda_function" "broker" { - architectures = ["arm64"] - filename = data.archive_file.placeholder.output_path - function_name = var.function_name - handler = "bootstrap" - memory_size = var.function_memory_size - role = aws_iam_role.execution.arn - runtime = "provided.al2023" - timeout = var.function_timeout - - source_code_hash = data.archive_file.placeholder.output_base64sha256 - - environment { - variables = { - GITHUB_TOKEN_BROKER_CLIENT_ID_PARAM = var.client_id_parameter_name - GITHUB_TOKEN_BROKER_INSTALLATION_ID_PARAM = var.installation_id_parameter_name - GITHUB_TOKEN_BROKER_LOG_LEVEL = var.log_level - GITHUB_TOKEN_BROKER_PRIVATE_KEY_PARAM = var.private_key_parameter_name - } - } - - lifecycle { - ignore_changes = [ - filename, - source_code_hash, - ] - } - - depends_on = [ - aws_cloudwatch_log_group.broker, - aws_iam_role_policy.execution_logs, - aws_iam_role_policy.execution_ssm, - ] - - tags = merge(local.common_tags, { - Name = var.function_name - }) -} diff --git a/aws/github-token-broker/locals.tf b/aws/github-token-broker/locals.tf deleted file mode 100644 index aac1454..0000000 --- a/aws/github-token-broker/locals.tf +++ /dev/null @@ -1,133 +0,0 @@ -locals { - common_tags = merge(var.tags, { - "glab:project" = "glab" - "glab:domain" = "aws" - "glab:purpose" = "github-token-broker" - }) - - ssm_parameter_names = { - client_id = var.client_id_parameter_name - installation_id = var.installation_id_parameter_name - private_key = var.private_key_parameter_name - } - - ssm_parameter_arns = { - for key, name in local.ssm_parameter_names : - key => "arn:aws:ssm:${var.aws_region}:${data.aws_caller_identity.current.account_id}:parameter${name}" - } - - github_oidc_host = replace(var.github_oidc_provider_url, "https://", "") - github_oidc_provider_arn = var.github_oidc_provider_arn != "" ? var.github_oidc_provider_arn : aws_iam_openid_connect_provider.github_actions[0].arn - - lambda_assume_role_policy = jsonencode({ - Version = "2012-10-17" - Statement = [ - { - Effect = "Allow" - Action = "sts:AssumeRole" - Principal = { - Service = "lambda.amazonaws.com" - } - }, - ] - }) - - lambda_logs_policy = jsonencode({ - Version = "2012-10-17" - Statement = [ - { - Sid = "AllowWriteTokenBrokerLogs" - Effect = "Allow" - Action = [ - "logs:CreateLogStream", - "logs:PutLogEvents", - ] - Resource = "${aws_cloudwatch_log_group.broker.arn}:*" - }, - ] - }) - - lambda_ssm_policy = jsonencode({ - Version = "2012-10-17" - Statement = [ - { - Sid = "AllowReadGitHubAppParameters" - Effect = "Allow" - Action = [ - "ssm:GetParameter", - "ssm:GetParameters", - ] - Resource = values(local.ssm_parameter_arns) - }, - ] - }) - - lambda_private_key_kms_policy = jsonencode({ - Version = "2012-10-17" - Statement = [ - { - Sid = "AllowDecryptGitHubAppPrivateKeyParameter" - Effect = "Allow" - Action = [ - "kms:Decrypt", - ] - Resource = var.private_key_kms_key_arn - }, - ] - }) - - publisher_assume_role_policy = jsonencode({ - Version = "2012-10-17" - Statement = [ - { - Effect = "Allow" - Action = "sts:AssumeRoleWithWebIdentity" - Principal = { - Federated = local.github_oidc_provider_arn - } - Condition = { - StringEquals = { - "${local.github_oidc_host}:aud" = var.github_oidc_audience - } - StringLike = { - "${local.github_oidc_host}:sub" = var.github_oidc_subject - } - } - }, - ] - }) - - publisher_policy = jsonencode({ - Version = "2012-10-17" - Statement = [ - { - Sid = "AllowPublishGitHubTokenBrokerCode" - Effect = "Allow" - Action = [ - "lambda:GetFunction", - "lambda:GetFunctionConfiguration", - "lambda:PublishVersion", - "lambda:UpdateFunctionCode", - ] - Resource = aws_lambda_function.broker.arn - }, - ] - }) - - invoke_policy = jsonencode({ - Version = "2012-10-17" - Statement = [ - { - Sid = "AllowInvokeGitHubTokenBroker" - Effect = "Allow" - Action = [ - "lambda:InvokeFunction", - ] - Resource = [ - aws_lambda_function.broker.arn, - "${aws_lambda_function.broker.arn}:*", - ] - }, - ] - }) -} diff --git a/aws/github-token-broker/moon.yml b/aws/github-token-broker/moon.yml deleted file mode 100644 index ecb101f..0000000 --- a/aws/github-token-broker/moon.yml +++ /dev/null @@ -1,25 +0,0 @@ -layer: 'application' -tags: - - 'aws' - - 'tofu' - -project: - title: 'AWS GitHub Token Broker' - description: 'OpenTofu stack for the AWS Lambda GitHub App token broker.' - owner: 'GilmanLab' - maintainers: - - 'josh' - -tasks: - check: - command: './scripts/check.sh' - toolchains: 'system' - inputs: - - '*.tf' - - '*.tfvars' - - '.terraform.lock.hcl' - - 'scripts/check.sh' - - 'tests/**/*.tftest.hcl' - options: - cache: false - runInCI: true diff --git a/aws/github-token-broker/outputs.tf b/aws/github-token-broker/outputs.tf deleted file mode 100644 index cec4abd..0000000 --- a/aws/github-token-broker/outputs.tf +++ /dev/null @@ -1,34 +0,0 @@ -output "execution_role_arn" { - description = "IAM role ARN used by the GitHub token broker Lambda." - value = aws_iam_role.execution.arn -} - -output "function_arn" { - description = "ARN of the GitHub token broker Lambda function." - value = aws_lambda_function.broker.arn -} - -output "function_name" { - description = "Name of the GitHub token broker Lambda function." - value = aws_lambda_function.broker.function_name -} - -output "github_oidc_provider_arn" { - description = "GitHub Actions OIDC provider ARN used by the publisher role." - value = local.github_oidc_provider_arn -} - -output "invoke_policy_arn" { - description = "IAM policy ARN that can be attached to bootstrap principals allowed to invoke the broker." - value = aws_iam_policy.invoke.arn -} - -output "publisher_role_arn" { - description = "IAM role ARN assumed by the platform release workflow to publish Lambda code." - value = aws_iam_role.publisher.arn -} - -output "ssm_parameter_names" { - description = "SSM parameter names read by the GitHub token broker Lambda." - value = local.ssm_parameter_names -} diff --git a/aws/github-token-broker/providers.tf b/aws/github-token-broker/providers.tf deleted file mode 100644 index 4a7e080..0000000 --- a/aws/github-token-broker/providers.tf +++ /dev/null @@ -1,10 +0,0 @@ -provider "aws" { - region = var.aws_region - - default_tags { - tags = { - "glab:managed-by" = "tofu" - "glab:stack" = "aws/github-token-broker" - } - } -} diff --git a/aws/github-token-broker/scripts/check.sh b/aws/github-token-broker/scripts/check.sh deleted file mode 100755 index f4403ce..0000000 --- a/aws/github-token-broker/scripts/check.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -TF_DATA_DIR="$(mktemp -d)" -trap 'rm -rf "$TF_DATA_DIR"' EXIT -export TF_DATA_DIR - -tofu fmt -check -recursive -tofu init -backend=false -input=false -tofu validate -tofu test diff --git a/aws/github-token-broker/terraform.tf b/aws/github-token-broker/terraform.tf deleted file mode 100644 index 955ff28..0000000 --- a/aws/github-token-broker/terraform.tf +++ /dev/null @@ -1,14 +0,0 @@ -terraform { - required_version = ">= 1.10, < 2.0" - - required_providers { - archive = { - source = "hashicorp/archive" - version = "~> 2.7" - } - aws = { - source = "hashicorp/aws" - version = "~> 5.90" - } - } -} diff --git a/aws/github-token-broker/tests/main.tftest.hcl b/aws/github-token-broker/tests/main.tftest.hcl deleted file mode 100644 index 36c5d73..0000000 --- a/aws/github-token-broker/tests/main.tftest.hcl +++ /dev/null @@ -1,155 +0,0 @@ -mock_provider "aws" { - alias = "mock" - - mock_data "aws_caller_identity" { - defaults = { - account_id = "123456789012" - } - } - - mock_resource "aws_iam_role" { - defaults = { - arn = "arn:aws:iam::123456789012:role/mock" - } - } - - mock_resource "aws_lambda_function" { - defaults = { - arn = "arn:aws:lambda:us-west-2:123456789012:function:glab-github-token-broker" - } - } -} - -run "plan_defaults" { - command = plan - - providers = { - aws = aws.mock - } - - assert { - condition = aws_lambda_function.broker.function_name == "glab-github-token-broker" - error_message = "The Lambda function name should match the platform publish workflow." - } - - assert { - condition = aws_lambda_function.broker.runtime == "provided.al2023" - error_message = "The Lambda should use the Amazon Linux 2023 custom runtime." - } - - assert { - condition = aws_lambda_function.broker.architectures[0] == "arm64" - error_message = "The Lambda should default to arm64." - } - - assert { - condition = aws_lambda_function.broker.environment[0].variables.GITHUB_TOKEN_BROKER_PRIVATE_KEY_PARAM == "/glab/bootstrap/github-app/private-key-pem" - error_message = "The Lambda should read the expected private-key SSM parameter." - } - - assert { - condition = aws_cloudwatch_log_group.broker.retention_in_days == 30 - error_message = "The Lambda log group should default to 30 day retention." - } - - assert { - condition = aws_iam_openid_connect_provider.github_actions[0].url == "https://token.actions.githubusercontent.com" - error_message = "The default stack should create a GitHub Actions OIDC provider." - } - - assert { - condition = contains(aws_iam_openid_connect_provider.github_actions[0].client_id_list, "sts.amazonaws.com") - error_message = "The GitHub Actions OIDC provider should trust the AWS STS audience." - } - - assert { - condition = jsondecode(aws_iam_role.publisher.assume_role_policy).Statement[0].Condition.StringLike["token.actions.githubusercontent.com:sub"] == "repo:GilmanLab/platform:ref:refs/tags/github-token-broker-v*" - error_message = "The publisher role should be scoped to github-token-broker release tags." - } - - assert { - condition = contains(jsondecode(aws_iam_role_policy.publisher.policy).Statement[0].Action, "lambda:UpdateFunctionCode") - error_message = "The publisher role should be able to update Lambda function code." - } - - assert { - condition = !contains(jsondecode(aws_iam_role_policy.publisher.policy).Statement[0].Action, "lambda:InvokeFunction") - error_message = "The publisher role should not be able to invoke the broker." - } - - assert { - condition = contains(jsondecode(aws_iam_role_policy.execution_ssm.policy).Statement[0].Resource, "arn:aws:ssm:us-west-2:123456789012:parameter/glab/bootstrap/github-app/private-key-pem") - error_message = "The execution role should be scoped to the GitHub App SSM parameters." - } -} - -run "plan_existing_oidc_provider" { - command = plan - - providers = { - aws = aws.mock - } - - variables { - github_oidc_provider_arn = "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com" - } - - assert { - condition = length(aws_iam_openid_connect_provider.github_actions) == 0 - error_message = "The stack should not create a duplicate OIDC provider when an ARN is supplied." - } - - assert { - condition = jsondecode(aws_iam_role.publisher.assume_role_policy).Statement[0].Principal.Federated == "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com" - error_message = "The publisher role should trust the supplied OIDC provider ARN." - } -} - -run "plan_private_key_cmk" { - command = plan - - providers = { - aws = aws.mock - } - - variables { - private_key_kms_key_arn = "arn:aws:kms:us-west-2:123456789012:key/00000000-0000-0000-0000-000000000000" - } - - assert { - condition = length(aws_iam_role_policy.execution_private_key_kms) == 1 - error_message = "Supplying a private-key CMK should add a KMS decrypt policy." - } -} - -run "reject_relative_private_key_parameter" { - command = plan - - providers = { - aws = aws.mock - } - - variables { - private_key_parameter_name = "glab/bootstrap/github-app/private-key-pem" - } - - expect_failures = [ - var.private_key_parameter_name, - ] -} - -run "reject_wrong_github_subject_repo" { - command = plan - - providers = { - aws = aws.mock - } - - variables { - github_oidc_subject = "repo:OtherOrg/platform:ref:refs/tags/github-token-broker-v*" - } - - expect_failures = [ - var.github_oidc_subject, - ] -} diff --git a/aws/github-token-broker/variables.tf b/aws/github-token-broker/variables.tf deleted file mode 100644 index 21d4963..0000000 --- a/aws/github-token-broker/variables.tf +++ /dev/null @@ -1,146 +0,0 @@ -variable "aws_region" { - description = "AWS region in which the GitHub token broker Lambda is created." - type = string - default = "us-west-2" -} - -variable "client_id_parameter_name" { - description = "SSM parameter that stores the GitHub App client ID." - type = string - default = "/glab/bootstrap/github-app/client-id" - - validation { - condition = startswith(var.client_id_parameter_name, "/") - error_message = "client_id_parameter_name must be an absolute SSM parameter path." - } -} - -variable "execution_role_name" { - description = "IAM role name used by the GitHub token broker Lambda execution role." - type = string - default = "glab-github-token-broker" -} - -variable "function_memory_size" { - description = "Memory size, in MiB, assigned to the GitHub token broker Lambda." - type = number - default = 128 - - validation { - condition = var.function_memory_size >= 128 && var.function_memory_size <= 10240 - error_message = "function_memory_size must be between 128 and 10240." - } -} - -variable "function_name" { - description = "Name of the GitHub token broker Lambda function." - type = string - default = "glab-github-token-broker" -} - -variable "function_timeout" { - description = "Timeout, in seconds, for the GitHub token broker Lambda." - type = number - default = 10 - - validation { - condition = var.function_timeout >= 1 && var.function_timeout <= 900 - error_message = "function_timeout must be between 1 and 900 seconds." - } -} - -variable "github_oidc_audience" { - description = "GitHub Actions OIDC audience allowed to assume the publisher role." - type = string - default = "sts.amazonaws.com" -} - -variable "github_oidc_provider_arn" { - description = "Existing GitHub Actions OIDC provider ARN. Leave empty to create the provider in this stack." - type = string - default = "" -} - -variable "github_oidc_provider_url" { - description = "GitHub Actions OIDC issuer URL." - type = string - default = "https://token.actions.githubusercontent.com" - - validation { - condition = startswith(var.github_oidc_provider_url, "https://") - error_message = "github_oidc_provider_url must be an HTTPS URL." - } -} - -variable "github_oidc_subject" { - description = "GitHub Actions OIDC subject pattern allowed to assume the publisher role." - type = string - default = "repo:GilmanLab/platform:ref:refs/tags/github-token-broker-v*" - - validation { - condition = startswith(var.github_oidc_subject, "repo:GilmanLab/platform:") - error_message = "github_oidc_subject must stay scoped to GilmanLab/platform." - } -} - -variable "installation_id_parameter_name" { - description = "SSM parameter that stores the GitHub App installation ID." - type = string - default = "/glab/bootstrap/github-app/installation-id" - - validation { - condition = startswith(var.installation_id_parameter_name, "/") - error_message = "installation_id_parameter_name must be an absolute SSM parameter path." - } -} - -variable "log_level" { - description = "Runtime log level passed to the GitHub token broker Lambda." - type = string - default = "info" - - validation { - condition = contains(["debug", "info", "warn", "error"], var.log_level) - error_message = "log_level must be one of debug, info, warn, or error." - } -} - -variable "log_retention_in_days" { - description = "CloudWatch log retention, in days, for the GitHub token broker Lambda log group." - type = number - default = 30 - - validation { - condition = contains([1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1096, 1827, 2192, 2557, 2922, 3288, 3653], var.log_retention_in_days) - error_message = "log_retention_in_days must be a CloudWatch Logs retention value." - } -} - -variable "private_key_kms_key_arn" { - description = "Optional customer-managed KMS key ARN for the private-key SSM SecureString. Leave empty while the parameter uses alias/aws/ssm." - type = string - default = "" -} - -variable "private_key_parameter_name" { - description = "SSM SecureString parameter that stores the GitHub App private key." - type = string - default = "/glab/bootstrap/github-app/private-key-pem" - - validation { - condition = startswith(var.private_key_parameter_name, "/") - error_message = "private_key_parameter_name must be an absolute SSM parameter path." - } -} - -variable "publisher_role_name" { - description = "IAM role name assumed by the platform release workflow to publish Lambda code." - type = string - default = "glab-github-token-broker-publisher" -} - -variable "tags" { - description = "Extra tags to merge onto all created resources." - type = map(string) - default = {} -} diff --git a/aws/keycloak/.gitignore b/aws/keycloak/.gitignore deleted file mode 100644 index eaecc08..0000000 --- a/aws/keycloak/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -.terraform/ -*.tfstate -*.tfstate.* -tfplan -crash.log -crash.*.log -override.tf -override.tf.json -*_override.tf -*_override.tf.json diff --git a/aws/keycloak/.terraform.lock.hcl b/aws/keycloak/.terraform.lock.hcl deleted file mode 100644 index 37fbe38..0000000 --- a/aws/keycloak/.terraform.lock.hcl +++ /dev/null @@ -1,38 +0,0 @@ -# This file is maintained automatically by "tofu init". -# Manual edits may be lost in future updates. - -provider "registry.opentofu.org/hashicorp/aws" { - version = "5.100.0" - constraints = ">= 5.0.0, ~> 5.90, < 7.0.0" - hashes = [ - "h1:BrNG7eFOdRrRRbHdvrTjMJ8X8Oh/tiegURiKf7J2db8=", - "zh:1a41f3ee26720fee7a9a0a361890632a1701b5dc1cf5355dc651ddbe115682ff", - "zh:30457f36690c19307921885cc5e72b9dbeba369445815903acd5c39ac0e41e7a", - "zh:42c22674d5f23f6309eaf3ac3a4f1f8b66b566c1efe1dcb0dd2fb30c17ce1f78", - "zh:4cc271c795ff8ce6479ec2d11a8ba65a0a9ed6331def6693f4b9dccb6e662838", - "zh:60932aa376bb8c87cd1971240063d9d38ba6a55502c867fdbb9f5361dc93d003", - "zh:864e42784bde77b18393ebfcc0104cea9123da5f4392e8a059789e296952eefa", - "zh:9750423138bb01ecaa5cec1a6691664f7783d301fb1628d3b64a231b6b564e0e", - "zh:e5d30c4dec271ef9d6fe09f48237ec6cfea1036848f835b4e47f274b48bda5a7", - "zh:e62bd314ae97b43d782e0841b13e68a3f8ec85cc762004f973ce5ce7b6cdbfd0", - "zh:ea851a3c072528a4445ac6236ba2ce58ffc99ec466019b0bd0e4adde63a248e4", - ] -} - -provider "registry.opentofu.org/hashicorp/null" { - version = "3.2.4" - constraints = ">= 3.2.0" - hashes = [ - "h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=", - "zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3", - "zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb", - "zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2", - "zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4", - "zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d", - "zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6", - "zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072", - "zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447", - "zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58", - "zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80", - ] -} diff --git a/aws/keycloak/Justfile b/aws/keycloak/Justfile deleted file mode 100644 index 4894a50..0000000 --- a/aws/keycloak/Justfile +++ /dev/null @@ -1,29 +0,0 @@ -set shell := ["bash", "-euo", "pipefail", "-c"] - -default: - @just --list - -# Offline validation: no backend or AWS credentials required. -check: - ./scripts/check.sh - -# Format Tofu files in place. -fmt: - tofu fmt -recursive - -# Initialize the working directory against the S3 backend. -init: - test -n "${GLAB_AWS_STATE_BUCKET:-}" || { echo "Set GLAB_AWS_STATE_BUCKET to the pre-created S3 backend bucket." >&2; exit 1; } - tofu init -reconfigure -backend-config="bucket=${GLAB_AWS_STATE_BUCKET}" - -# Render and save a plan to `tfplan`. -plan: - tofu plan -out=tfplan - -# Apply the saved plan produced by `just plan`. -apply: - tofu apply tfplan - -# Show outputs from the last applied state. -output: - tofu output diff --git a/aws/keycloak/README.md b/aws/keycloak/README.md deleted file mode 100644 index c8ad6d1..0000000 --- a/aws/keycloak/README.md +++ /dev/null @@ -1,139 +0,0 @@ -# aws/keycloak - -OpenTofu stack for the AWS-hosted Keycloak instance in the `lab` account. - -This stack creates: - -- a dedicated `t4g.small` Flatcar Container Linux EC2 instance -- a dedicated encrypted gp3 data volume mounted at `/var/lib/keycloak` -- an IAM role and instance profile for SSM management -- a security group that exposes HTTPS only to lab CIDRs -- a private Route 53 `A` record for `id.glab.lol` -- scoped Route 53 permissions for ACME DNS-01 validation in `acme.glab.lol` -- a GitHub token broker Lambda, sourced from `meigma/github-token-broker`, for short-lived `GilmanLab/secrets` access -- Keycloak instance-role permission to invoke the token broker -- Keycloak instance-role permission to decrypt only SOPS secrets with `Repo=GilmanLab/secrets` and `Scope=keycloak` -- Ignition-managed systemd units for Postgres, Keycloak, Traefik, bootstrap secret fetches, and realm configuration - -This stack configures the `lab` realm, one local admin account with password plus WebAuthn/YubiKey enrollment, and a minimal public OIDC client for Incus. It does **not** configure GitHub OIDC, OpenFGA, backups, Synology sync, Kubernetes OIDC, Argo CD, Grafana, or IAM Identity Center federation. - -Traefik obtains the `id.glab.lol` certificate from Let's Encrypt through DNS-01. Cloudflare delegates `_acme-challenge.id.glab.lol` to the public Route 53 `acme.glab.lol` zone, and the Keycloak instance role may mutate only the delegated TXT record for this hostname. - -## Bootstrap model - -Flatcar receives raw Ignition JSON through EC2 user data. Ignition writes non-secret runtime config and helper scripts under `/etc/glab/keycloak`, then enables the systemd units. - -`glab-keycloak-bootstrap.service` runs the pinned `labctl` container: - -```sh -secrets get services/keycloak/bootstrap.sops.yaml \ - --source github \ - --field /stack_env \ - --output /run/glab/keycloak/stack.env \ - --aws-region us-west-2 \ - --broker-function glab-github-token-broker -``` - -Plaintext bootstrap material is written only under `/run/glab/keycloak`. The persistent data volume stores Postgres state in `/var/lib/keycloak/postgres` and ACME state in `/var/lib/keycloak/acme`; the Postgres directory is owned by the container's Postgres UID. - -`glab-keycloak-config.service` runs after Keycloak is healthy. It renders the -realm config, compares its SHA-256 hash with -`/var/lib/keycloak/config/lab-realm.sha256`, and skips when the stored hash -matches. When the hash changes, it fetches -`services/keycloak/admin.sops.yaml` through the same broker-backed `labctl` -path, writes `/run/glab/keycloak/admin.env`, then runs pinned -`keycloak-config-cli` to create the `lab` realm, local admin user, and -touch-only WebAuthn policy. It also creates the public `incus` OIDC client with -OAuth 2.0 Device Authorization Grant enabled and the confidential -`glab-keycloak-config` service account used for later imports after the -temporary master bootstrap admin is disabled. The service writes the new -realm-config hash only after a successful import. - -The follow-up Incus-side OIDC values are: - -- `oidc.issuer = https://id.glab.lol/realms/lab` -- `oidc.client.id = incus` -- `oidc.scopes = openid,email,profile` -- `oidc.claim = preferred_username` if validation confirms that Keycloak emits - it in the Incus access token - -After enrolling and validating the admin YubiKey in the browser, disable the -temporary master bootstrap admin manually: - -```sh -aws ssm send-command \ - --document-name AWS-RunShellScript \ - --targets Key=instanceids,Values="$(tofu output -raw instance_id)" \ - --parameters commands='["systemctl start glab-keycloak-disable-bootstrap-admin.service"]' -``` - -## Prerequisites - -- OpenTofu `>= 1.10` -- `just` -- `AWS_PROFILE` set to the `lab` account admin profile -- `GLAB_AWS_STATE_BUCKET` set to the pre-created S3 backend bucket in the `lab` account -- the `aws/lab-foundation` and `aws/subnet-router` stacks already applied -- the GitHub App SSM parameters created outside Terraform: - `/glab/bootstrap/github-app/client-id`, - `/glab/bootstrap/github-app/installation-id`, and - `/glab/bootstrap/github-app/private-key-pem` -- `gh` and `sha256sum` on the apply host so the broker module can download and verify the pinned release asset -- Cloudflare delegates `acme.glab.lol` to the `aws/lab-foundation` `acme_zone_name_servers` output and sets `_acme-challenge.id.glab.lol` as a CNAME to `_acme-challenge.id.acme.glab.lol` -- `secrets/services/keycloak/bootstrap.sops.yaml` exists in `GilmanLab/secrets` with SOPS KMS context `Repo=GilmanLab/secrets` and `Scope=keycloak` -- `secrets/services/keycloak/admin.sops.yaml` exists in `GilmanLab/secrets` with SOPS KMS context `Repo=GilmanLab/secrets` and `Scope=keycloak` - -The expected local operator flow is to export both AWS values via `direnv`. - -## Legacy token broker cleanup - -The old `aws/github-token-broker` stack used the same default Lambda name, `glab-github-token-broker`, and was originally published from `GilmanLab/platform`. Do not apply both stacks at the same time. - -If the old Lambda is still active, retire it before applying this stack: - -```sh -cd ../github-token-broker -just init -tofu plan -destroy \ - -target=aws_lambda_function.broker \ - -target=aws_cloudwatch_log_group.broker \ - -target=aws_iam_policy.invoke \ - -target=aws_iam_role.execution \ - -target=aws_iam_role.publisher \ - -target=aws_iam_role_policy.execution_logs \ - -target=aws_iam_role_policy.execution_ssm \ - -target=aws_iam_role_policy.publisher \ - -out=destroy-broker-only.tfplan -tofu apply destroy-broker-only.tfplan -``` - -Then apply `aws/keycloak`. This removes the legacy Lambda resources while leaving the GitHub Actions OIDC provider alone; only run a full destroy after confirming that provider has no shared use. - -## Usage - -```sh -just check -just init -just plan -just apply -``` - -`just init` uses `GLAB_AWS_STATE_BUCKET` to finish the otherwise-partial S3 backend configuration. The backend bucket itself is part of the manual AWS bootstrap and is intentionally not managed by this stack. - -After apply, use SSM to inspect the host without printing secret values: - -```sh -aws ssm send-command \ - --document-name AWS-RunShellScript \ - --targets Key=instanceids,Values="$(tofu output -raw instance_id)" \ - --parameters commands='[ - "systemctl is-active glab-keycloak-bootstrap.service glab-keycloak-postgres.service glab-keycloak.service glab-keycloak-traefik.service", - "systemctl is-active glab-keycloak-config.service || systemctl status --no-pager glab-keycloak-config.service", - "findmnt /var/lib/keycloak", - "stat -c %a\\ %s\\ %n /run/glab/keycloak/stack.env", - "stat -c %a\\ %s\\ %n /run/glab/keycloak/admin.env", - "cut -d= -f1 /run/glab/keycloak/stack.env | sort", - "cut -d= -f1 /run/glab/keycloak/admin.env | sort", - "curl -fsS http://127.0.0.1:9000/health/ready" - ]' -``` diff --git a/aws/keycloak/backend.tf b/aws/keycloak/backend.tf deleted file mode 100644 index e531b39..0000000 --- a/aws/keycloak/backend.tf +++ /dev/null @@ -1,8 +0,0 @@ -terraform { - backend "s3" { - key = "aws/keycloak.tfstate" - region = "us-west-2" - encrypt = true - use_lockfile = true - } -} diff --git a/aws/keycloak/compute.tf b/aws/keycloak/compute.tf deleted file mode 100644 index 1add7f4..0000000 --- a/aws/keycloak/compute.tf +++ /dev/null @@ -1,44 +0,0 @@ -resource "aws_instance" "keycloak" { - ami = var.flatcar_ami_id - associate_public_ip_address = true - iam_instance_profile = aws_iam_instance_profile.keycloak.name - instance_type = var.instance_type - subnet_id = data.aws_subnet.public.id - user_data = local.ignition_config - user_data_replace_on_change = true - vpc_security_group_ids = [aws_security_group.keycloak.id] - - metadata_options { - http_endpoint = "enabled" - http_put_response_hop_limit = 2 - http_tokens = "required" - } - - root_block_device { - delete_on_termination = true - encrypted = true - volume_size = var.root_volume_size - volume_type = "gp3" - } - - tags = merge(local.common_tags, { - Name = var.instance_name - }) -} - -resource "aws_ebs_volume" "keycloak_data" { - availability_zone = data.aws_subnet.public.availability_zone - encrypted = true - size = var.data_volume_size - type = "gp3" - - tags = merge(local.common_tags, { - Name = "${var.instance_name}-data" - }) -} - -resource "aws_volume_attachment" "keycloak_data" { - device_name = var.data_volume_device_name - instance_id = aws_instance.keycloak.id - volume_id = aws_ebs_volume.keycloak_data.id -} diff --git a/aws/keycloak/data.tf b/aws/keycloak/data.tf deleted file mode 100644 index 97a8355..0000000 --- a/aws/keycloak/data.tf +++ /dev/null @@ -1,154 +0,0 @@ -data "aws_iam_policy_document" "keycloak_assume_role" { - statement { - actions = ["sts:AssumeRole"] - - principals { - identifiers = ["ec2.amazonaws.com"] - type = "Service" - } - } -} - -data "aws_iam_policy_document" "keycloak_sops_decrypt" { - statement { - sid = "AllowDecryptKeycloakSopsSecrets" - actions = [ - "kms:Decrypt", - ] - resources = [ - var.sops_kms_key_arn, - ] - - condition { - test = "StringEquals" - variable = "kms:EncryptionContext:Repo" - values = [ - var.sops_kms_context_repo, - ] - } - - condition { - test = "StringEquals" - variable = "kms:EncryptionContext:Scope" - values = [ - var.sops_kms_context_scope, - ] - } - } -} - -data "aws_iam_policy_document" "keycloak_acme_route53" { - statement { - sid = "AllowAcmeChangePolling" - actions = [ - "route53:GetChange", - ] - resources = [ - "arn:aws:route53:::change/*", - ] - } - - statement { - sid = "AllowAcmeZoneDiscovery" - actions = [ - "route53:ListHostedZonesByName", - ] - resources = ["*"] - } - - statement { - sid = "AllowReadAcmeZoneRecords" - actions = [ - "route53:ListResourceRecordSets", - ] - resources = [ - data.aws_route53_zone.acme.arn, - ] - } - - statement { - sid = "AllowWriteAcmeChallengeTxt" - actions = [ - "route53:ChangeResourceRecordSets", - ] - resources = [ - data.aws_route53_zone.acme.arn, - ] - - condition { - test = "ForAllValues:StringEquals" - variable = "route53:ChangeResourceRecordSetsNormalizedRecordNames" - values = [ - local.acme_challenge_record_name, - ] - } - - condition { - test = "ForAllValues:StringEquals" - variable = "route53:ChangeResourceRecordSetsRecordTypes" - values = [ - "TXT", - ] - } - } -} - -data "aws_route53_zone" "private" { - name = var.private_zone_name - private_zone = true -} - -data "aws_route53_zone" "acme" { - name = var.acme_zone_name - private_zone = false -} - -data "aws_route_table" "public" { - filter { - name = "tag:Name" - values = [var.public_route_table_name] - } - - filter { - name = "vpc-id" - values = [data.aws_vpc.lab.id] - } -} - -data "aws_instance" "subnet_router" { - filter { - name = "tag:Name" - values = [var.subnet_router_instance_name] - } -} - -data "aws_network_interface" "subnet_router" { - filter { - name = "attachment.instance-id" - values = [data.aws_instance.subnet_router.id] - } - - filter { - name = "attachment.device-index" - values = ["0"] - } -} - -data "aws_subnet" "public" { - filter { - name = "tag:Name" - values = [var.public_subnet_name] - } - - filter { - name = "vpc-id" - values = [data.aws_vpc.lab.id] - } -} - -data "aws_vpc" "lab" { - filter { - name = "tag:Name" - values = [var.vpc_name] - } -} diff --git a/aws/keycloak/github_token_broker.tf b/aws/keycloak/github_token_broker.tf deleted file mode 100644 index 3c80e19..0000000 --- a/aws/keycloak/github_token_broker.tf +++ /dev/null @@ -1,39 +0,0 @@ -module "github_token_broker" { - source = "github.com/meigma/github-token-broker//terraform?ref=v2.0.0" - - function_name = var.github_token_broker_function_name - repository_owner = "GilmanLab" - repository_name = "secrets" - - release_repository = var.github_token_broker_release_repository - lambda_artifact = { - release_version = var.github_token_broker_release_version - } - - ssm_parameter_paths = var.github_token_broker_ssm_parameter_paths - kms_key_arn = var.github_token_broker_private_key_kms_key_arn - permissions = var.github_token_broker_permissions - log_retention_days = var.github_token_broker_log_retention_days - - tags = merge(local.common_tags, { - "glab:purpose" = "keycloak-github-token-broker" - }) -} - -data "aws_iam_policy_document" "keycloak_github_token_broker_invoke" { - statement { - sid = "AllowInvokeGitHubTokenBroker" - actions = [ - "lambda:InvokeFunction", - ] - resources = [ - module.github_token_broker.function_arn, - ] - } -} - -resource "aws_iam_role_policy" "keycloak_github_token_broker_invoke" { - name = "${var.iam_role_name}-github-token-broker-invoke" - policy = data.aws_iam_policy_document.keycloak_github_token_broker_invoke.json - role = aws_iam_role.keycloak.id -} diff --git a/aws/keycloak/iam.tf b/aws/keycloak/iam.tf deleted file mode 100644 index db88540..0000000 --- a/aws/keycloak/iam.tf +++ /dev/null @@ -1,30 +0,0 @@ -resource "aws_iam_role" "keycloak" { - assume_role_policy = data.aws_iam_policy_document.keycloak_assume_role.json - name = var.iam_role_name - - tags = merge(local.common_tags, { - Name = var.iam_role_name - }) -} - -resource "aws_iam_role_policy" "keycloak_acme_route53" { - name = "${var.iam_role_name}-acme-route53" - policy = data.aws_iam_policy_document.keycloak_acme_route53.json - role = aws_iam_role.keycloak.id -} - -resource "aws_iam_role_policy" "keycloak_sops_decrypt" { - name = "${var.iam_role_name}-sops-keycloak-decrypt" - policy = data.aws_iam_policy_document.keycloak_sops_decrypt.json - role = aws_iam_role.keycloak.id -} - -resource "aws_iam_role_policy_attachment" "keycloak_ssm_managed_instance_core" { - policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore" - role = aws_iam_role.keycloak.name -} - -resource "aws_iam_instance_profile" "keycloak" { - name = var.iam_role_name - role = aws_iam_role.keycloak.name -} diff --git a/aws/keycloak/locals.tf b/aws/keycloak/locals.tf deleted file mode 100644 index bea2629..0000000 --- a/aws/keycloak/locals.tf +++ /dev/null @@ -1,250 +0,0 @@ -locals { - common_tags = merge(var.tags, { - "glab:project" = "glab" - "glab:domain" = "aws" - "glab:purpose" = "keycloak" - }) - - private_hostname_relative = trimsuffix(var.private_hostname, ".${var.private_zone_name}") - acme_challenge_record_name = "_acme-challenge.${local.private_hostname_relative}.${var.acme_zone_name}" - traefik_certificate_resolver = "letsencrypt" - traefik_acme_storage_file = "/etc/traefik/acme/acme.json" - traefik_acme_storage_host_dir = "${var.data_dir}/acme" - postgres_state_dir = "${var.data_dir}/postgres" - keycloak_env_path = "${var.bootstrap_runtime_dir}/keycloak.env" - helper_script_dir = "${var.runtime_dir}/bin" - keycloak_config_dir = "${var.data_dir}/config" - keycloak_config_marker_path = "${local.keycloak_config_dir}/lab-realm.sha256" - keycloak_realm_config_path = "${var.bootstrap_runtime_dir}/lab-realm.json" - data_volume_device_path = "/dev/disk/by-id/nvme-Amazon_Elastic_Block_Store_${replace(aws_ebs_volume.keycloak_data.id, "-", "")}" - - traefik_dynamic_config = templatefile("${path.module}/templates/traefik_dynamic.yml.tftpl", { - private_hostname = var.private_hostname - traefik_certificate_resolver = local.traefik_certificate_resolver - }) - - lab_realm_config = templatefile("${path.module}/templates/keycloak/lab-realm.json.tftpl", { - private_hostname = var.private_hostname - }) - - bootstrap_unit_name = "glab-keycloak-bootstrap.service" - config_unit_name = "glab-keycloak-config.service" - data_unit_name = "glab-keycloak-data.service" - disable_bootstrap_admin_unit_name = "glab-keycloak-disable-bootstrap-admin.service" - network_unit_name = "glab-keycloak-network.service" - postgres_unit_name = "glab-keycloak-postgres.service" - keycloak_unit_name = "glab-keycloak.service" - traefik_unit_name = "glab-keycloak-traefik.service" - - runtime_template_vars = { - acme_ca_server = var.acme_ca_server - acme_email = var.acme_email - aws_region = var.aws_region - bootstrap_field = var.bootstrap_field - bootstrap_output_path = var.bootstrap_output_path - bootstrap_runtime_dir = var.bootstrap_runtime_dir - bootstrap_secret_path = var.bootstrap_secret_path - bootstrap_unit_name = local.bootstrap_unit_name - config_field = var.config_field - config_output_path = var.config_output_path - config_secret_path = var.config_secret_path - config_unit_name = local.config_unit_name - data_dir = var.data_dir - data_unit_name = local.data_unit_name - data_volume_device_path = local.data_volume_device_path - data_volume_label = var.data_volume_label - disable_bootstrap_admin_unit_name = local.disable_bootstrap_admin_unit_name - github_token_broker_function_name = var.github_token_broker_function_name - helper_script_dir = local.helper_script_dir - keycloak_config_cli_image = var.keycloak_config_cli_image - keycloak_config_dir = local.keycloak_config_dir - keycloak_config_marker_path = local.keycloak_config_marker_path - keycloak_env_path = local.keycloak_env_path - keycloak_image = var.keycloak_image - keycloak_realm_config_path = local.keycloak_realm_config_path - lab_realm_config_gzip_base64 = base64gzip(local.lab_realm_config) - keycloak_unit_name = local.keycloak_unit_name - labctl_image = var.labctl_image - network_unit_name = local.network_unit_name - postgres_image = var.postgres_image - postgres_state_dir = local.postgres_state_dir - postgres_unit_name = local.postgres_unit_name - runtime_dir = var.runtime_dir - traefik_acme_storage_file = local.traefik_acme_storage_file - traefik_acme_storage_host_dir = local.traefik_acme_storage_host_dir - traefik_certificate_resolver = local.traefik_certificate_resolver - traefik_dns_challenge_resolvers = var.traefik_dns_challenge_resolvers - traefik_image = var.traefik_image - traefik_unit_name = local.traefik_unit_name - traefik_zone_id = data.aws_route53_zone.acme.zone_id - } - - prepare_data_script = templatefile("${path.module}/templates/scripts/prepare-data.sh.tftpl", local.runtime_template_vars) - create_network_script = templatefile("${path.module}/templates/scripts/create-network.sh.tftpl", local.runtime_template_vars) - run_postgres_script = templatefile("${path.module}/templates/scripts/run-postgres.sh.tftpl", local.runtime_template_vars) - wait_postgres_script = templatefile("${path.module}/templates/scripts/wait-postgres.sh.tftpl", local.runtime_template_vars) - run_keycloak_script = templatefile("${path.module}/templates/scripts/run-keycloak.sh.tftpl", local.runtime_template_vars) - run_traefik_script = templatefile("${path.module}/templates/scripts/run-traefik.sh.tftpl", local.runtime_template_vars) - run_config_script = templatefile("${path.module}/templates/scripts/run-keycloak-config.sh.tftpl", local.runtime_template_vars) - disable_admin_script = templatefile("${path.module}/templates/scripts/disable-bootstrap-admin.sh.tftpl", local.runtime_template_vars) - - data_unit = templatefile("${path.module}/templates/systemd/${local.data_unit_name}.tftpl", local.runtime_template_vars) - network_unit = templatefile("${path.module}/templates/systemd/${local.network_unit_name}.tftpl", local.runtime_template_vars) - bootstrap_unit = templatefile("${path.module}/templates/systemd/${local.bootstrap_unit_name}.tftpl", local.runtime_template_vars) - postgres_unit = templatefile("${path.module}/templates/systemd/${local.postgres_unit_name}.tftpl", local.runtime_template_vars) - keycloak_unit = templatefile("${path.module}/templates/systemd/${local.keycloak_unit_name}.tftpl", local.runtime_template_vars) - traefik_unit = templatefile("${path.module}/templates/systemd/${local.traefik_unit_name}.tftpl", local.runtime_template_vars) - config_unit = templatefile("${path.module}/templates/systemd/${local.config_unit_name}.tftpl", local.runtime_template_vars) - disable_admin_unit = templatefile( - "${path.module}/templates/systemd/${local.disable_bootstrap_admin_unit_name}.tftpl", - local.runtime_template_vars, - ) - - ignition_files = [ - { - path = "${var.runtime_dir}/traefik_dynamic.yml" - mode = 420 - contents = { - source = "data:text/plain;charset=utf-8;base64,${base64encode(local.traefik_dynamic_config)}" - } - }, - { - path = "${local.helper_script_dir}/prepare-data.sh" - mode = 493 - contents = { - source = "data:text/plain;charset=utf-8;base64,${base64encode(local.prepare_data_script)}" - } - }, - { - path = "${local.helper_script_dir}/create-network.sh" - mode = 493 - contents = { - source = "data:text/plain;charset=utf-8;base64,${base64encode(local.create_network_script)}" - } - }, - { - path = "${local.helper_script_dir}/run-postgres.sh" - mode = 493 - contents = { - source = "data:text/plain;charset=utf-8;base64,${base64encode(local.run_postgres_script)}" - } - }, - { - path = "${local.helper_script_dir}/wait-postgres.sh" - mode = 493 - contents = { - source = "data:text/plain;charset=utf-8;base64,${base64encode(local.wait_postgres_script)}" - } - }, - { - path = "${local.helper_script_dir}/run-keycloak.sh" - mode = 493 - contents = { - source = "data:text/plain;charset=utf-8;base64,${base64encode(local.run_keycloak_script)}" - } - }, - { - path = "${local.helper_script_dir}/run-traefik.sh" - mode = 493 - contents = { - source = "data:text/plain;charset=utf-8;base64,${base64encode(local.run_traefik_script)}" - } - }, - { - path = "${local.helper_script_dir}/run-keycloak-config.sh" - mode = 493 - contents = { - source = "data:text/plain;charset=utf-8;base64,${base64encode(local.run_config_script)}" - } - }, - { - path = "${local.helper_script_dir}/disable-bootstrap-admin.sh" - mode = 493 - contents = { - source = "data:text/plain;charset=utf-8;base64,${base64encode(local.disable_admin_script)}" - } - }, - ] - - ignition_payload_config = jsonencode({ - ignition = { - version = "3.3.0" - } - storage = { - directories = [ - { - path = "/etc/glab" - mode = 493 - }, - { - path = var.runtime_dir - mode = 493 - }, - { - path = local.helper_script_dir - mode = 493 - }, - ] - files = local.ignition_files - } - systemd = { - units = [ - { - enabled = true - name = "amazon-ssm-agent.service" - }, - { - contents = local.data_unit - enabled = true - name = local.data_unit_name - }, - { - contents = local.network_unit - enabled = true - name = local.network_unit_name - }, - { - contents = local.bootstrap_unit - enabled = true - name = local.bootstrap_unit_name - }, - { - contents = local.postgres_unit - enabled = true - name = local.postgres_unit_name - }, - { - contents = local.keycloak_unit - enabled = true - name = local.keycloak_unit_name - }, - { - contents = local.config_unit - enabled = true - name = local.config_unit_name - }, - { - contents = local.traefik_unit - enabled = true - name = local.traefik_unit_name - }, - { - contents = local.disable_admin_unit - name = local.disable_bootstrap_admin_unit_name - }, - ] - } - }) - - ignition_config = jsonencode({ - ignition = { - version = "3.3.0" - config = { - replace = { - source = "data:application/vnd.coreos.ignition+json;base64,${base64gzip(local.ignition_payload_config)}" - compression = "gzip" - } - } - } - }) -} diff --git a/aws/keycloak/moon.yml b/aws/keycloak/moon.yml deleted file mode 100644 index 44f9e96..0000000 --- a/aws/keycloak/moon.yml +++ /dev/null @@ -1,26 +0,0 @@ -layer: 'application' -tags: - - 'aws' - - 'tofu' - -project: - title: 'AWS Keycloak' - description: 'OpenTofu stack for the AWS EC2 Keycloak instance.' - owner: 'GilmanLab' - maintainers: - - 'josh' - -tasks: - check: - command: './scripts/check.sh' - toolchains: 'system' - inputs: - - '*.tf' - - '*.tfvars' - - '.terraform.lock.hcl' - - 'scripts/check.sh' - - 'templates/**/*.tftpl' - - 'tests/**/*.tftest.hcl' - options: - cache: false - runInCI: true diff --git a/aws/keycloak/network.tf b/aws/keycloak/network.tf deleted file mode 100644 index 6ce0510..0000000 --- a/aws/keycloak/network.tf +++ /dev/null @@ -1,59 +0,0 @@ -resource "aws_security_group" "keycloak" { - description = "Security group for the AWS Keycloak instance." - name = var.security_group_name - vpc_id = data.aws_vpc.lab.id - - tags = merge(local.common_tags, { - Name = var.security_group_name - }) -} - -resource "aws_vpc_security_group_egress_rule" "keycloak_ipv4" { - cidr_ipv4 = "0.0.0.0/0" - ip_protocol = "-1" - security_group_id = aws_security_group.keycloak.id -} - -resource "aws_vpc_security_group_egress_rule" "keycloak_ipv6" { - cidr_ipv6 = "::/0" - ip_protocol = "-1" - security_group_id = aws_security_group.keycloak.id -} - -resource "aws_vpc_security_group_ingress_rule" "keycloak_https" { - for_each = var.lab_cidrs - - cidr_ipv4 = each.value - description = "Allow lab-side HTTPS traffic from ${each.value}." - from_port = 443 - ip_protocol = "tcp" - security_group_id = aws_security_group.keycloak.id - to_port = 443 -} - -resource "aws_vpc_security_group_ingress_rule" "operator_tailscale_https" { - for_each = var.operator_tailscale_cidrs - - cidr_ipv4 = each.value - description = "Allow direct operator HTTPS traffic from ${each.key}." - from_port = 443 - ip_protocol = "tcp" - security_group_id = aws_security_group.keycloak.id - to_port = 443 -} - -resource "aws_route53_record" "private" { - name = var.private_hostname - records = [aws_instance.keycloak.private_ip] - ttl = var.dns_record_ttl - type = "A" - zone_id = data.aws_route53_zone.private.zone_id -} - -resource "aws_route" "operator_tailscale" { - for_each = var.operator_tailscale_cidrs - - destination_cidr_block = each.value - network_interface_id = data.aws_network_interface.subnet_router.id - route_table_id = data.aws_route_table.public.id -} diff --git a/aws/keycloak/outputs.tf b/aws/keycloak/outputs.tf deleted file mode 100644 index da6098c..0000000 --- a/aws/keycloak/outputs.tf +++ /dev/null @@ -1,80 +0,0 @@ -output "dns_name" { - description = "Private DNS name for the Keycloak service." - value = aws_route53_record.private.fqdn -} - -output "iam_role_arn" { - description = "IAM role ARN used by the Keycloak instance." - value = aws_iam_role.keycloak.arn -} - -output "github_token_broker_function_arn" { - description = "ARN of the GitHub token broker Lambda deployed for Keycloak bootstrap access." - value = module.github_token_broker.function_arn -} - -output "github_token_broker_function_name" { - description = "Name of the GitHub token broker Lambda deployed for Keycloak bootstrap access." - value = module.github_token_broker.function_name -} - -output "github_token_broker_log_group_name" { - description = "CloudWatch log group for the GitHub token broker Lambda." - value = module.github_token_broker.log_group_name -} - -output "github_token_broker_release_version" { - description = "GitHub token broker release version deployed by this stack." - value = module.github_token_broker.deployed_version -} - -output "instance_id" { - description = "EC2 instance ID of the Keycloak host." - value = aws_instance.keycloak.id -} - -output "private_ip" { - description = "Private IPv4 address of the Keycloak host." - value = aws_instance.keycloak.private_ip -} - -output "public_ip" { - description = "Public IPv4 address associated with the Keycloak host for outbound bootstrap traffic." - value = aws_instance.keycloak.public_ip -} - -output "security_group_id" { - description = "Security group attached to the Keycloak host." - value = aws_security_group.keycloak.id -} - -output "data_volume_id" { - description = "Encrypted EBS volume ID mounted at /var/lib/keycloak." - value = aws_ebs_volume.keycloak_data.id -} - -output "bootstrap_unit_name" { - description = "Systemd unit that fetches Keycloak bootstrap secrets." - value = local.bootstrap_unit_name -} - -output "service_unit_names" { - description = "Systemd units that run the Flatcar Keycloak stack." - value = { - data = local.data_unit_name - network = local.network_unit_name - postgres = local.postgres_unit_name - keycloak = local.keycloak_unit_name - traefik = local.traefik_unit_name - } -} - -output "acme_challenge_record_name" { - description = "Route 53 TXT record name Traefik may mutate for Keycloak ACME DNS-01 validation." - value = local.acme_challenge_record_name -} - -output "acme_zone_id" { - description = "Public Route 53 hosted zone ID used for Keycloak ACME DNS-01 validation." - value = data.aws_route53_zone.acme.zone_id -} diff --git a/aws/keycloak/providers.tf b/aws/keycloak/providers.tf deleted file mode 100644 index a4757ad..0000000 --- a/aws/keycloak/providers.tf +++ /dev/null @@ -1,10 +0,0 @@ -provider "aws" { - region = var.aws_region - - default_tags { - tags = { - "glab:managed-by" = "tofu" - "glab:stack" = "aws/keycloak" - } - } -} diff --git a/aws/keycloak/scripts/check.sh b/aws/keycloak/scripts/check.sh deleted file mode 100755 index f4403ce..0000000 --- a/aws/keycloak/scripts/check.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -TF_DATA_DIR="$(mktemp -d)" -trap 'rm -rf "$TF_DATA_DIR"' EXIT -export TF_DATA_DIR - -tofu fmt -check -recursive -tofu init -backend=false -input=false -tofu validate -tofu test diff --git a/aws/keycloak/templates/keycloak/lab-realm.json.tftpl b/aws/keycloak/templates/keycloak/lab-realm.json.tftpl deleted file mode 100644 index f569e54..0000000 --- a/aws/keycloak/templates/keycloak/lab-realm.json.tftpl +++ /dev/null @@ -1,190 +0,0 @@ -{ - "realm": "lab", - "enabled": true, - "displayName": "glab", - "registrationAllowed": false, - "resetPasswordAllowed": false, - "rememberMe": false, - "loginWithEmailAllowed": false, - "duplicateEmailsAllowed": false, - "verifyEmail": false, - "browserFlow": "glab browser", - "webAuthnPolicyRpEntityName": "glab", - "webAuthnPolicyRpId": "${private_hostname}", - "webAuthnPolicySignatureAlgorithms": [ - "ES256", - "RS256" - ], - "webAuthnPolicyAttestationConveyancePreference": "none", - "webAuthnPolicyAuthenticatorAttachment": "not specified", - "webAuthnPolicyRequireResidentKey": "not specified", - "webAuthnPolicyUserVerificationRequirement": "discouraged", - "webAuthnPolicyCreateTimeout": 0, - "webAuthnPolicyAvoidSameAuthenticatorRegister": true, - "authenticationFlows": [ - { - "alias": "glab browser", - "description": "Browser flow for local password plus WebAuthn when enrolled.", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": false, - "authenticationExecutions": [ - { - "authenticator": "auth-cookie", - "requirement": "ALTERNATIVE", - "priority": 10, - "userSetupAllowed": false, - "authenticatorFlow": false - }, - { - "authenticator": "identity-provider-redirector", - "requirement": "ALTERNATIVE", - "priority": 20, - "userSetupAllowed": false, - "authenticatorFlow": false - }, - { - "flowAlias": "glab browser forms", - "requirement": "ALTERNATIVE", - "priority": 30, - "userSetupAllowed": false, - "authenticatorFlow": true - } - ] - }, - { - "alias": "glab browser forms", - "description": "Local password form with conditional WebAuthn second factor.", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": false, - "authenticationExecutions": [ - { - "authenticator": "auth-username-password-form", - "requirement": "REQUIRED", - "priority": 10, - "userSetupAllowed": false, - "authenticatorFlow": false - }, - { - "flowAlias": "glab webauthn conditional", - "requirement": "CONDITIONAL", - "priority": 20, - "userSetupAllowed": false, - "authenticatorFlow": true - } - ] - }, - { - "alias": "glab webauthn conditional", - "description": "Require WebAuthn when the user has a WebAuthn credential.", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": false, - "authenticationExecutions": [ - { - "authenticator": "conditional-user-configured", - "requirement": "REQUIRED", - "priority": 10, - "userSetupAllowed": false, - "authenticatorFlow": false - }, - { - "authenticator": "webauthn-authenticator", - "requirement": "REQUIRED", - "priority": 20, - "userSetupAllowed": false, - "authenticatorFlow": false - } - ] - } - ], - "requiredActions": [ - { - "alias": "webauthn-register", - "name": "WebAuthn Register", - "providerId": "webauthn-register", - "enabled": true, - "defaultAction": false, - "priority": 20, - "config": {} - } - ], - "clients": [ - { - "clientId": "glab-keycloak-config", - "name": "glab Keycloak Config", - "description": "Confidential automation client for applying declarative lab realm configuration.", - "enabled": true, - "protocol": "openid-connect", - "publicClient": false, - "bearerOnly": false, - "standardFlowEnabled": false, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": false, - "serviceAccountsEnabled": true, - "authorizationServicesEnabled": false, - "frontchannelLogout": false, - "secret": "$(env:KEYCLOAK_CONFIG_CLIENT_SECRET)" - }, - { - "clientId": "incus", - "name": "Incus", - "description": "Public OIDC client for Incus human authentication.", - "enabled": true, - "protocol": "openid-connect", - "publicClient": true, - "bearerOnly": false, - "standardFlowEnabled": false, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": false, - "serviceAccountsEnabled": false, - "authorizationServicesEnabled": false, - "frontchannelLogout": false, - "attributes": { - "oauth2.device.authorization.grant.enabled": "true" - }, - "defaultClientScopes": [ - "profile", - "email" - ], - "optionalClientScopes": [ - "offline_access" - ] - } - ], - "users": [ - { - "username": "service-account-glab-keycloak-config", - "enabled": true, - "serviceAccountClientId": "glab-keycloak-config", - "clientRoles": { - "realm-management": [ - "realm-admin" - ] - } - }, - { - "username": "$(env:KEYCLOAK_LOCAL_ADMIN_USERNAME)", - "email": "$(env:KEYCLOAK_LOCAL_ADMIN_EMAIL)", - "emailVerified": true, - "enabled": true, - "requiredActions": [ - "webauthn-register" - ], - "credentials": [ - { - "type": "password", - "userLabel": "initial", - "value": "$(env:KEYCLOAK_LOCAL_ADMIN_PASSWORD)", - "temporary": false - } - ], - "clientRoles": { - "realm-management": [ - "realm-admin" - ] - } - } - ] -} diff --git a/aws/keycloak/templates/scripts/create-network.sh.tftpl b/aws/keycloak/templates/scripts/create-network.sh.tftpl deleted file mode 100644 index 0cfdb0c..0000000 --- a/aws/keycloak/templates/scripts/create-network.sh.tftpl +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/sh -set -eu - -if ! /usr/bin/docker network inspect keycloak >/dev/null 2>&1; then - /usr/bin/docker network create keycloak >/dev/null -fi diff --git a/aws/keycloak/templates/scripts/disable-bootstrap-admin.sh.tftpl b/aws/keycloak/templates/scripts/disable-bootstrap-admin.sh.tftpl deleted file mode 100644 index 6fe52a0..0000000 --- a/aws/keycloak/templates/scripts/disable-bootstrap-admin.sh.tftpl +++ /dev/null @@ -1,50 +0,0 @@ -#!/bin/sh -set -eu - -. '${bootstrap_output_path}' - -deadline=$(( $(date +%s) + 300 )) -until /usr/bin/curl -fsS http://127.0.0.1:9000/health/ready >/dev/null; do - if [ "$(date +%s)" -ge "$deadline" ]; then - echo 'Timed out waiting for Keycloak readiness.' >&2 - exit 1 - fi - sleep 5 -done - -/usr/bin/docker run --rm \ - --network keycloak \ - -e KC_BOOTSTRAP_ADMIN_USERNAME="$KC_BOOTSTRAP_ADMIN_USERNAME" \ - -e KC_BOOTSTRAP_ADMIN_PASSWORD="$KC_BOOTSTRAP_ADMIN_PASSWORD" \ - --entrypoint /bin/sh \ - '${keycloak_image}' \ - -c 'set -eu - /opt/keycloak/bin/kcadm.sh config credentials \ - --server http://keycloak:8080 \ - --realm master \ - --user "$KC_BOOTSTRAP_ADMIN_USERNAME" \ - --password "$KC_BOOTSTRAP_ADMIN_PASSWORD" >/dev/null - - user_id="$( - /opt/keycloak/bin/kcadm.sh get users \ - -r master \ - -q username="$KC_BOOTSTRAP_ADMIN_USERNAME" \ - --fields id \ - --format csv \ - --noquotes | - { - line= - IFS= read -r line || true - if [ "$line" = "id" ]; then - IFS= read -r line || true - fi - printf "%s" "$line" - } - )" - if [ -z "$user_id" ]; then - echo "Could not find bootstrap admin user." >&2 - exit 1 - fi - - /opt/keycloak/bin/kcadm.sh update "users/$user_id" -r master -s enabled=false - ' diff --git a/aws/keycloak/templates/scripts/prepare-data.sh.tftpl b/aws/keycloak/templates/scripts/prepare-data.sh.tftpl deleted file mode 100644 index 3a6e21c..0000000 --- a/aws/keycloak/templates/scripts/prepare-data.sh.tftpl +++ /dev/null @@ -1,31 +0,0 @@ -#!/bin/sh -set -eu - -dev='${data_volume_device_path}' -i=0 -while [ ! -e "$dev" ] && [ "$i" -lt 90 ]; do - i=$((i + 1)) - sleep 2 -done - -if [ ! -e "$dev" ]; then - echo "data volume $dev did not appear" >&2 - exit 1 -fi - -if ! blkid "$dev" >/dev/null 2>&1; then - mkfs.ext4 -F -L '${data_volume_label}' "$dev" -fi - -mkdir -p '${data_dir}' -if ! findmnt -rn '${data_dir}' >/dev/null 2>&1; then - mount "$dev" '${data_dir}' -fi - -mkdir -p '${postgres_state_dir}' '${traefik_acme_storage_host_dir}' -chown 999:999 '${postgres_state_dir}' -chmod 0750 '${data_dir}' -chmod 0700 '${postgres_state_dir}' -chmod 0700 '${traefik_acme_storage_host_dir}' -touch '${traefik_acme_storage_host_dir}/acme.json' -chmod 0600 '${traefik_acme_storage_host_dir}/acme.json' diff --git a/aws/keycloak/templates/scripts/run-keycloak-config.sh.tftpl b/aws/keycloak/templates/scripts/run-keycloak-config.sh.tftpl deleted file mode 100644 index b098eaa..0000000 --- a/aws/keycloak/templates/scripts/run-keycloak-config.sh.tftpl +++ /dev/null @@ -1,80 +0,0 @@ -#!/bin/sh -set -eu - -. '${bootstrap_output_path}' - -mkdir -p '${bootstrap_runtime_dir}' '${keycloak_config_dir}' -chmod 0700 '${bootstrap_runtime_dir}' -rm -f '${config_output_path}' '${keycloak_realm_config_path}' '${keycloak_realm_config_path}.gz.b64' - -cat >'${keycloak_realm_config_path}.gz.b64' <<'EOF_REALM_CONFIG' -${lab_realm_config_gzip_base64} -EOF_REALM_CONFIG -/usr/bin/base64 -d '${keycloak_realm_config_path}.gz.b64' | /usr/bin/gzip -d >'${keycloak_realm_config_path}' -chmod 0600 '${keycloak_realm_config_path}' -rm -f '${keycloak_realm_config_path}.gz.b64' - -desired_hash="$(/usr/bin/sha256sum '${keycloak_realm_config_path}' | /usr/bin/awk '{print $1}')" -if [ -f '${keycloak_config_marker_path}' ] && [ "$(cat '${keycloak_config_marker_path}')" = "$desired_hash" ]; then - echo "Keycloak lab realm configuration already at $desired_hash." - rm -f '${keycloak_realm_config_path}' - exit 0 -fi - -/usr/bin/docker run --rm \ - --network host \ - --user 0:0 \ - -v '${bootstrap_runtime_dir}:${bootstrap_runtime_dir}' \ - '${labctl_image}' \ - secrets get '${config_secret_path}' \ - --source github \ - --field '${config_field}' \ - --output '${config_output_path}' \ - --aws-region '${aws_region}' \ - --broker-function '${github_token_broker_function_name}' -chmod 0600 '${config_output_path}' -. '${config_output_path}' - -deadline=$(( $(date +%s) + 300 )) -until /usr/bin/curl -fsS http://127.0.0.1:9000/health/ready >/dev/null; do - if [ "$(date +%s)" -ge "$deadline" ]; then - echo 'Timed out waiting for Keycloak readiness.' >&2 - exit 1 - fi - sleep 5 -done - -run_import() { - /usr/bin/docker rm -f keycloak-config-cli >/dev/null 2>&1 || true - /usr/bin/docker run --rm \ - --name keycloak-config-cli \ - --network keycloak \ - --user 0:0 \ - --env-file '${config_output_path}' \ - "$@" \ - -e KEYCLOAK_URL='http://keycloak:8080' \ - -e KEYCLOAK_AVAILABILITYCHECK_ENABLED='true' \ - -e KEYCLOAK_AVAILABILITYCHECK_TIMEOUT='120s' \ - -e IMPORT_FILES_LOCATIONS='/config/lab-realm.json' \ - -e IMPORT_VARSUBSTITUTION_ENABLED='true' \ - -e IMPORT_VARSUBSTITUTION_UNDEFINEDISERROR='true' \ - -v '${keycloak_realm_config_path}:/config/lab-realm.json:ro' \ - '${keycloak_config_cli_image}' -} - -if [ -f '${keycloak_config_marker_path}' ]; then - run_import \ - -e KEYCLOAK_LOGINREALM='lab' \ - -e KEYCLOAK_CLIENTID='glab-keycloak-config' \ - -e KEYCLOAK_CLIENTSECRET="$KEYCLOAK_CONFIG_CLIENT_SECRET" \ - -e KEYCLOAK_GRANTTYPE='client_credentials' -else - run_import \ - -e KEYCLOAK_LOGINREALM='master' \ - -e KEYCLOAK_USER="$KC_BOOTSTRAP_ADMIN_USERNAME" \ - -e KEYCLOAK_PASSWORD="$KC_BOOTSTRAP_ADMIN_PASSWORD" -fi - -printf '%s\n' "$desired_hash" >'${keycloak_config_marker_path}' -chmod 0600 '${keycloak_config_marker_path}' -rm -f '${keycloak_realm_config_path}' diff --git a/aws/keycloak/templates/scripts/run-keycloak.sh.tftpl b/aws/keycloak/templates/scripts/run-keycloak.sh.tftpl deleted file mode 100644 index 8fd7357..0000000 --- a/aws/keycloak/templates/scripts/run-keycloak.sh.tftpl +++ /dev/null @@ -1,35 +0,0 @@ -#!/bin/sh -set -eu - -. '${bootstrap_output_path}' -umask 077 -{ - printf 'POSTGRES_DB=%s\n' "$POSTGRES_DB" - printf 'POSTGRES_USER=%s\n' "$POSTGRES_USER" - printf 'POSTGRES_PASSWORD=%s\n' "$POSTGRES_PASSWORD" - printf 'KC_BOOTSTRAP_ADMIN_USERNAME=%s\n' "$KC_BOOTSTRAP_ADMIN_USERNAME" - printf 'KC_BOOTSTRAP_ADMIN_PASSWORD=%s\n' "$KC_BOOTSTRAP_ADMIN_PASSWORD" - printf 'KC_HEAP_MIN=%s\n' "$KC_HEAP_MIN" - printf 'KC_HEAP_MAX=%s\n' "$KC_HEAP_MAX" - printf 'KC_DB=postgres\n' - printf 'KC_DB_URL=jdbc:postgresql://postgres:5432/%s\n' "$POSTGRES_DB" - printf 'KC_DB_USERNAME=%s\n' "$POSTGRES_USER" - printf 'KC_DB_PASSWORD=%s\n' "$POSTGRES_PASSWORD" - printf 'KC_HEALTH_ENABLED=true\n' - printf 'KC_HOSTNAME=https://%s\n' "$KC_HOSTNAME" - printf 'KC_HTTP_ENABLED=true\n' - printf 'KC_PROXY_HEADERS=xforwarded\n' - printf 'JAVA_OPTS_KC_HEAP=-Xms%s -Xmx%s\n' "$KC_HEAP_MIN" "$KC_HEAP_MAX" -} >'${keycloak_env_path}' -chmod 0600 '${keycloak_env_path}' - -/usr/bin/docker rm -f keycloak >/dev/null 2>&1 || true -exec /usr/bin/docker run \ - --name keycloak \ - --network keycloak \ - --network-alias keycloak \ - --env-file '${keycloak_env_path}' \ - --publish 127.0.0.1:9000:9000 \ - --pull always \ - '${keycloak_image}' \ - start diff --git a/aws/keycloak/templates/scripts/run-postgres.sh.tftpl b/aws/keycloak/templates/scripts/run-postgres.sh.tftpl deleted file mode 100644 index 6836eff..0000000 --- a/aws/keycloak/templates/scripts/run-postgres.sh.tftpl +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/sh -set -eu - -/usr/bin/docker rm -f keycloak-postgres >/dev/null 2>&1 || true -exec /usr/bin/docker run \ - --name keycloak-postgres \ - --network keycloak \ - --network-alias postgres \ - --env-file '${bootstrap_output_path}' \ - --env PGDATA=/var/lib/postgresql/data \ - --volume '${postgres_state_dir}:/var/lib/postgresql' \ - --health-cmd 'pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB"' \ - --health-interval 10s \ - --health-timeout 5s \ - --health-retries 12 \ - --pull always \ - '${postgres_image}' diff --git a/aws/keycloak/templates/scripts/run-traefik.sh.tftpl b/aws/keycloak/templates/scripts/run-traefik.sh.tftpl deleted file mode 100644 index 19cb04c..0000000 --- a/aws/keycloak/templates/scripts/run-traefik.sh.tftpl +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/sh -set -eu - -/usr/bin/docker rm -f keycloak-traefik >/dev/null 2>&1 || true -exec /usr/bin/docker run \ - --name keycloak-traefik \ - --network keycloak \ - --env AWS_REGION='${aws_region}' \ - --env AWS_HOSTED_ZONE_ID='${traefik_zone_id}' \ - --publish 443:443 \ - --volume '${traefik_acme_storage_host_dir}:/etc/traefik/acme' \ - --volume '${runtime_dir}/traefik_dynamic.yml:/etc/traefik/dynamic.yml:ro' \ - --pull always \ - '${traefik_image}' \ - --entrypoints.websecure.address=:443 \ - --certificatesresolvers.${traefik_certificate_resolver}.acme.email='${acme_email}' \ - --certificatesresolvers.${traefik_certificate_resolver}.acme.storage='${traefik_acme_storage_file}' \ - --certificatesresolvers.${traefik_certificate_resolver}.acme.caserver='${acme_ca_server}' \ - --certificatesresolvers.${traefik_certificate_resolver}.acme.dnschallenge.provider=route53 \ - --certificatesresolvers.${traefik_certificate_resolver}.acme.dnschallenge.resolvers='${traefik_dns_challenge_resolvers}' \ - --providers.file.filename=/etc/traefik/dynamic.yml \ - --providers.file.watch=true \ - --api.dashboard=false \ - --log.level=INFO diff --git a/aws/keycloak/templates/scripts/wait-postgres.sh.tftpl b/aws/keycloak/templates/scripts/wait-postgres.sh.tftpl deleted file mode 100644 index dbbb6e9..0000000 --- a/aws/keycloak/templates/scripts/wait-postgres.sh.tftpl +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/sh -set -eu - -i=0 -while [ "$i" -lt 90 ]; do - if /usr/bin/docker exec keycloak-postgres pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB" >/dev/null 2>&1; then - exit 0 - fi - i=$((i + 1)) - sleep 2 -done - -echo "postgres did not become ready" >&2 -exit 1 diff --git a/aws/keycloak/templates/systemd/glab-keycloak-bootstrap.service.tftpl b/aws/keycloak/templates/systemd/glab-keycloak-bootstrap.service.tftpl deleted file mode 100644 index ceb1d3c..0000000 --- a/aws/keycloak/templates/systemd/glab-keycloak-bootstrap.service.tftpl +++ /dev/null @@ -1,17 +0,0 @@ -[Unit] -Description=Fetch Keycloak bootstrap secrets with labctl -Wants=network-online.target -Requires=docker.service ${data_unit_name} -After=network-online.target docker.service ${data_unit_name} -Before=${postgres_unit_name} ${keycloak_unit_name} ${traefik_unit_name} - -[Service] -Type=oneshot -RemainAfterExit=yes -ExecStartPre=/usr/bin/mkdir -p ${bootstrap_runtime_dir} -ExecStartPre=/usr/bin/chmod 0700 ${bootstrap_runtime_dir} -ExecStartPre=/usr/bin/rm -f ${bootstrap_output_path} ${keycloak_env_path} -ExecStart=/usr/bin/docker run --rm --network host --user 0:0 -v ${bootstrap_runtime_dir}:${bootstrap_runtime_dir} ${labctl_image} secrets get ${bootstrap_secret_path} --source github --field ${bootstrap_field} --output ${bootstrap_output_path} --aws-region ${aws_region} --broker-function ${github_token_broker_function_name} - -[Install] -WantedBy=multi-user.target diff --git a/aws/keycloak/templates/systemd/glab-keycloak-config.service.tftpl b/aws/keycloak/templates/systemd/glab-keycloak-config.service.tftpl deleted file mode 100644 index 0a6ac55..0000000 --- a/aws/keycloak/templates/systemd/glab-keycloak-config.service.tftpl +++ /dev/null @@ -1,11 +0,0 @@ -[Unit] -Description=Configure Keycloak lab realm -Requires=docker.service ${data_unit_name} ${network_unit_name} ${bootstrap_unit_name} ${keycloak_unit_name} -After=docker.service ${data_unit_name} ${network_unit_name} ${bootstrap_unit_name} ${keycloak_unit_name} - -[Service] -Type=oneshot -ExecStart=${helper_script_dir}/run-keycloak-config.sh - -[Install] -WantedBy=multi-user.target diff --git a/aws/keycloak/templates/systemd/glab-keycloak-data.service.tftpl b/aws/keycloak/templates/systemd/glab-keycloak-data.service.tftpl deleted file mode 100644 index 4c2eca2..0000000 --- a/aws/keycloak/templates/systemd/glab-keycloak-data.service.tftpl +++ /dev/null @@ -1,11 +0,0 @@ -[Unit] -Description=Prepare Keycloak data volume -Before=${bootstrap_unit_name} ${postgres_unit_name} ${traefik_unit_name} - -[Service] -Type=oneshot -RemainAfterExit=yes -ExecStart=${helper_script_dir}/prepare-data.sh - -[Install] -WantedBy=multi-user.target diff --git a/aws/keycloak/templates/systemd/glab-keycloak-disable-bootstrap-admin.service.tftpl b/aws/keycloak/templates/systemd/glab-keycloak-disable-bootstrap-admin.service.tftpl deleted file mode 100644 index bf70419..0000000 --- a/aws/keycloak/templates/systemd/glab-keycloak-disable-bootstrap-admin.service.tftpl +++ /dev/null @@ -1,9 +0,0 @@ -[Unit] -Description=Disable temporary Keycloak master bootstrap admin -Requires=docker.service ${network_unit_name} ${bootstrap_unit_name} ${keycloak_unit_name} -After=docker.service ${network_unit_name} ${bootstrap_unit_name} ${keycloak_unit_name} -ConditionPathExists=${keycloak_config_marker_path} - -[Service] -Type=oneshot -ExecStart=${helper_script_dir}/disable-bootstrap-admin.sh diff --git a/aws/keycloak/templates/systemd/glab-keycloak-network.service.tftpl b/aws/keycloak/templates/systemd/glab-keycloak-network.service.tftpl deleted file mode 100644 index 4444076..0000000 --- a/aws/keycloak/templates/systemd/glab-keycloak-network.service.tftpl +++ /dev/null @@ -1,13 +0,0 @@ -[Unit] -Description=Create Keycloak Docker network -Requires=docker.service -After=docker.service -Before=${postgres_unit_name} ${keycloak_unit_name} ${traefik_unit_name} - -[Service] -Type=oneshot -RemainAfterExit=yes -ExecStart=${helper_script_dir}/create-network.sh - -[Install] -WantedBy=multi-user.target diff --git a/aws/keycloak/templates/systemd/glab-keycloak-postgres.service.tftpl b/aws/keycloak/templates/systemd/glab-keycloak-postgres.service.tftpl deleted file mode 100644 index 08f9e92..0000000 --- a/aws/keycloak/templates/systemd/glab-keycloak-postgres.service.tftpl +++ /dev/null @@ -1,16 +0,0 @@ -[Unit] -Description=Keycloak Postgres container -Requires=docker.service ${data_unit_name} ${network_unit_name} ${bootstrap_unit_name} -After=docker.service ${data_unit_name} ${network_unit_name} ${bootstrap_unit_name} - -[Service] -Restart=always -RestartSec=10 -EnvironmentFile=${bootstrap_output_path} -ExecStart=${helper_script_dir}/run-postgres.sh -ExecStop=-/usr/bin/docker stop keycloak-postgres -ExecStopPost=-/usr/bin/docker rm -f keycloak-postgres -TimeoutStartSec=0 - -[Install] -WantedBy=multi-user.target diff --git a/aws/keycloak/templates/systemd/glab-keycloak-traefik.service.tftpl b/aws/keycloak/templates/systemd/glab-keycloak-traefik.service.tftpl deleted file mode 100644 index e0c38b4..0000000 --- a/aws/keycloak/templates/systemd/glab-keycloak-traefik.service.tftpl +++ /dev/null @@ -1,15 +0,0 @@ -[Unit] -Description=Keycloak Traefik container -Requires=docker.service ${network_unit_name} ${data_unit_name} ${keycloak_unit_name} -After=docker.service ${network_unit_name} ${data_unit_name} ${keycloak_unit_name} - -[Service] -Restart=always -RestartSec=10 -ExecStart=${helper_script_dir}/run-traefik.sh -ExecStop=-/usr/bin/docker stop keycloak-traefik -ExecStopPost=-/usr/bin/docker rm -f keycloak-traefik -TimeoutStartSec=0 - -[Install] -WantedBy=multi-user.target diff --git a/aws/keycloak/templates/systemd/glab-keycloak.service.tftpl b/aws/keycloak/templates/systemd/glab-keycloak.service.tftpl deleted file mode 100644 index 483be55..0000000 --- a/aws/keycloak/templates/systemd/glab-keycloak.service.tftpl +++ /dev/null @@ -1,17 +0,0 @@ -[Unit] -Description=Keycloak container -Requires=docker.service ${network_unit_name} ${bootstrap_unit_name} ${postgres_unit_name} -After=docker.service ${network_unit_name} ${bootstrap_unit_name} ${postgres_unit_name} - -[Service] -Restart=always -RestartSec=10 -EnvironmentFile=${bootstrap_output_path} -ExecStartPre=${helper_script_dir}/wait-postgres.sh -ExecStart=${helper_script_dir}/run-keycloak.sh -ExecStop=-/usr/bin/docker stop keycloak -ExecStopPost=-/usr/bin/docker rm -f keycloak -TimeoutStartSec=0 - -[Install] -WantedBy=multi-user.target diff --git a/aws/keycloak/templates/traefik_dynamic.yml.tftpl b/aws/keycloak/templates/traefik_dynamic.yml.tftpl deleted file mode 100644 index 3311742..0000000 --- a/aws/keycloak/templates/traefik_dynamic.yml.tftpl +++ /dev/null @@ -1,15 +0,0 @@ -http: - routers: - keycloak: - rule: Host(`${private_hostname}`) - entryPoints: - - websecure - service: keycloak - tls: - certResolver: ${traefik_certificate_resolver} - - services: - keycloak: - loadBalancer: - servers: - - url: http://keycloak:8080 diff --git a/aws/keycloak/terraform.tf b/aws/keycloak/terraform.tf deleted file mode 100644 index b7a66eb..0000000 --- a/aws/keycloak/terraform.tf +++ /dev/null @@ -1,10 +0,0 @@ -terraform { - required_version = ">= 1.10, < 2.0" - - required_providers { - aws = { - source = "hashicorp/aws" - version = "~> 5.90" - } - } -} diff --git a/aws/keycloak/terraform.tfvars b/aws/keycloak/terraform.tfvars deleted file mode 100644 index 06f062d..0000000 --- a/aws/keycloak/terraform.tfvars +++ /dev/null @@ -1,6 +0,0 @@ -aws_region = "us-west-2" - -operator_tailscale_cidrs = { - macbook_pro = "100.123.79.63/32" - mac_studio = "100.122.142.76/32" -} diff --git a/aws/keycloak/tests/main.tftest.hcl b/aws/keycloak/tests/main.tftest.hcl deleted file mode 100644 index d4acc7a..0000000 --- a/aws/keycloak/tests/main.tftest.hcl +++ /dev/null @@ -1,485 +0,0 @@ -mock_provider "aws" { - alias = "mock" - - mock_data "aws_iam_policy_document" { - defaults = { - json = "{\"Version\":\"2012-10-17\",\"Statement\":[]}" - } - } - - mock_resource "aws_ebs_volume" { - defaults = { - id = "vol-0123456789abcdef0" - } - } - - mock_resource "aws_iam_role" { - defaults = { - arn = "arn:aws:iam::123456789012:role/mock-role" - id = "mock-role" - } - } - - mock_data "aws_route53_zone" { - defaults = { - arn = "arn:aws:route53:::hostedzone/Z00000000000000000" - zone_id = "Z00000000000000000" - } - } - - mock_data "aws_route_table" { - defaults = { - id = "rtb-00000000" - } - } - - mock_data "aws_instance" { - defaults = { - id = "i-0000000000000000" - } - } - - mock_data "aws_network_interface" { - defaults = { - id = "eni-00000000000000000" - } - } - - mock_data "aws_subnet" { - defaults = { - availability_zone = "us-west-2a" - id = "subnet-00000000" - } - } - - mock_data "aws_vpc" { - defaults = { - id = "vpc-00000000" - } - } -} - -run "plan_defaults" { - command = plan - - providers = { - aws = aws.mock - } - - assert { - condition = aws_instance.keycloak.ami == "ami-0ce605082061bbb10" - error_message = "The Keycloak host should default to the current Flatcar stable arm64 AMI for us-west-2." - } - - assert { - condition = aws_instance.keycloak.instance_type == "t4g.small" - error_message = "The default instance type should match the design doc." - } - - assert { - condition = aws_instance.keycloak.associate_public_ip_address == true - error_message = "The Keycloak host should have a public IPv4 address for outbound bootstrap traffic." - } - - assert { - condition = aws_instance.keycloak.metadata_options[0].http_tokens == "required" - error_message = "The Keycloak host should require IMDSv2." - } - - assert { - condition = aws_instance.keycloak.metadata_options[0].http_put_response_hop_limit == 2 - error_message = "The Keycloak host should allow containerized labctl to reach IMDSv2." - } - - assert { - condition = aws_instance.keycloak.root_block_device[0].encrypted == true && aws_instance.keycloak.root_block_device[0].volume_type == "gp3" && aws_instance.keycloak.root_block_device[0].volume_size == 16 - error_message = "The Keycloak root volume should be an encrypted 16 GiB gp3 volume." - } - - assert { - condition = aws_ebs_volume.keycloak_data.encrypted == true && aws_ebs_volume.keycloak_data.type == "gp3" && aws_ebs_volume.keycloak_data.size == 8 - error_message = "The Keycloak data volume should default to encrypted 8 GiB gp3." - } - - assert { - condition = aws_volume_attachment.keycloak_data.device_name == "/dev/xvdf" && aws_volume_attachment.keycloak_data.volume_id == aws_ebs_volume.keycloak_data.id - error_message = "The Keycloak data volume should attach to the Flatcar host." - } - - assert { - condition = strcontains(local.ignition_config, "\"compression\":\"gzip\"") && strcontains(local.ignition_config, "data:application/vnd.coreos.ignition+json;base64,") - error_message = "The Keycloak host should pass a compressed Ignition v3 config through EC2 user data." - } - - assert { - condition = strcontains(local.ignition_payload_config, "amazon-ssm-agent.service") - error_message = "The Keycloak host should enable the Flatcar AWS SSM agent." - } - - assert { - condition = strcontains(local.bootstrap_unit, "ghcr.io/gilmanlab/platform/labctl@sha256:4638b36a168df88d4206d5ff23aed62a6d8459ba7a2481c0b7c65c696445c1ec") - error_message = "The bootstrap unit should use the pinned labctl 0.2.0 image digest." - } - - assert { - condition = strcontains(local.bootstrap_unit, "--network host --user 0:0") - error_message = "The bootstrap container should run with host networking and root inside the container." - } - - assert { - condition = strcontains(local.bootstrap_unit, "secrets get services/keycloak/bootstrap.sops.yaml --source github --field /stack_env --output /run/glab/keycloak/stack.env") - error_message = "The bootstrap unit should fetch only the Keycloak stack_env field into /run." - } - - assert { - condition = strcontains(local.run_config_script, "secrets get 'services/keycloak/admin.sops.yaml'") && strcontains(local.run_config_script, "--field '/admin_env'") && strcontains(local.run_config_script, "--output '/run/glab/keycloak/admin.env'") - error_message = "The config script should fetch only the Keycloak admin_env field into /run." - } - - assert { - condition = strcontains(local.run_config_script, "sha256sum '/run/glab/keycloak/lab-realm.json'") && strcontains(local.run_config_script, "cat '/var/lib/keycloak/config/lab-realm.sha256'") && strcontains(local.run_config_script, "printf '%s\\n' \"$desired_hash\" >'/var/lib/keycloak/config/lab-realm.sha256'") - error_message = "The config script should use a rendered realm hash marker instead of a permanent first-import marker." - } - - assert { - condition = strcontains(local.run_config_script, "KEYCLOAK_LOGINREALM='lab'") && strcontains(local.run_config_script, "KEYCLOAK_CLIENTID='glab-keycloak-config'") && strcontains(local.run_config_script, "KEYCLOAK_CLIENTSECRET=\"$KEYCLOAK_CONFIG_CLIENT_SECRET\"") && strcontains(local.run_config_script, "KEYCLOAK_GRANTTYPE='client_credentials'") - error_message = "The config script should use the dedicated config service account after the first successful hash-marked import." - } - - assert { - condition = !strcontains(local.config_unit, "ConditionPathExists") - error_message = "The config unit should run on boot and let the config script decide whether the realm config changed." - } - - assert { - condition = strcontains(local.run_config_script, "quay.io/adorsys/keycloak-config-cli@sha256:2d2a0663cf324379d9ffab896db8d00293cd0326151968b319cf166f6eec8fca") - error_message = "The config script should use the pinned keycloak-config-cli image digest." - } - - assert { - condition = strcontains(local.run_config_script, "--name keycloak-config-cli") && strcontains(local.run_config_script, "--user 0:0") - error_message = "The config-cli container should run as root so it can read root-owned generated config files." - } - - assert { - condition = strcontains(local.lab_realm_config, "\"realm\": \"lab\"") && strcontains(local.lab_realm_config, "\"webAuthnPolicyRpId\": \"id.glab.lol\"") && strcontains(local.lab_realm_config, "\"webAuthnPolicyUserVerificationRequirement\": \"discouraged\"") - error_message = "The realm config should define the lab realm with touch-only WebAuthn policy for id.glab.lol." - } - - assert { - condition = strcontains(local.lab_realm_config, "\"username\": \"$(env:KEYCLOAK_LOCAL_ADMIN_USERNAME)\"") && strcontains(local.lab_realm_config, "\"value\": \"$(env:KEYCLOAK_LOCAL_ADMIN_PASSWORD)\"") && strcontains(local.lab_realm_config, "\"webauthn-register\"") - error_message = "The realm config should create the local admin from runtime env and require WebAuthn registration." - } - - assert { - condition = strcontains(local.lab_realm_config, "\"clientId\": \"glab-keycloak-config\"") && strcontains(local.lab_realm_config, "\"serviceAccountsEnabled\": true") && strcontains(local.lab_realm_config, "\"secret\": \"$(env:KEYCLOAK_CONFIG_CLIENT_SECRET)\"") - error_message = "The realm config should define a confidential service account client for future config imports." - } - - assert { - condition = strcontains(local.lab_realm_config, "\"username\": \"service-account-glab-keycloak-config\"") && strcontains(local.lab_realm_config, "\"serviceAccountClientId\": \"glab-keycloak-config\"") && strcontains(local.lab_realm_config, "\"realm-admin\"") - error_message = "The config service account should receive realm-admin privileges through realm-management." - } - - assert { - condition = strcontains(local.lab_realm_config, "\"clientId\": \"incus\"") && strcontains(local.lab_realm_config, "\"publicClient\": true") && strcontains(local.lab_realm_config, "\"oauth2.device.authorization.grant.enabled\": \"true\"") - error_message = "The realm config should define a public Incus OIDC client with device authorization enabled." - } - - assert { - condition = strcontains(local.lab_realm_config, "\"standardFlowEnabled\": false") && strcontains(local.lab_realm_config, "\"implicitFlowEnabled\": false") && strcontains(local.lab_realm_config, "\"directAccessGrantsEnabled\": false") && strcontains(local.lab_realm_config, "\"serviceAccountsEnabled\": false") && strcontains(local.lab_realm_config, "\"authorizationServicesEnabled\": false") - error_message = "The Incus client should keep unused OIDC grants and authorization services disabled." - } - - assert { - condition = strcontains(local.prepare_data_script, "/var/lib/keycloak/postgres") && strcontains(local.prepare_data_script, "/var/lib/keycloak/acme") && strcontains(local.prepare_data_script, "chown 999:999") - error_message = "The data preparation script should place Postgres and ACME state on the data volume." - } - - assert { - condition = strcontains(local.ignition_payload_config, "/etc/glab/keycloak/bin/run-keycloak.sh") && !strcontains(local.ignition_payload_config, "/usr/local/lib/glab-keycloak") - error_message = "Ignition should write helper scripts under writable root-backed config paths, not Flatcar's read-only /usr tree." - } - - assert { - condition = strcontains(local.ignition_payload_config, "/etc/glab/keycloak/bin/run-keycloak-config.sh") && strcontains(local.ignition_payload_config, "glab-keycloak-config.service") - error_message = "Ignition should install and enable the first-boot Keycloak config service." - } - - assert { - condition = strcontains(local.ignition_payload_config, "/etc/glab/keycloak/bin/disable-bootstrap-admin.sh") && strcontains(local.ignition_payload_config, "glab-keycloak-disable-bootstrap-admin.service") - error_message = "Ignition should install the manual bootstrap admin disable service." - } - - assert { - condition = strcontains(local.run_keycloak_script, "/run/glab/keycloak/keycloak.env") && strcontains(local.run_keycloak_script, "KC_DB_PASSWORD") - error_message = "The Keycloak runner should derive container-only env from the /run bootstrap secret." - } - - assert { - condition = strcontains(local.run_traefik_script, "--certificatesresolvers.letsencrypt.acme.dnschallenge.provider=route53") - error_message = "Traefik should use the Route 53 DNS-01 provider." - } - - assert { - condition = strcontains(local.run_traefik_script, "AWS_HOSTED_ZONE_ID='Z00000000000000000'") - error_message = "Traefik should receive the delegated Route 53 zone ID." - } - - assert { - condition = strcontains(local.traefik_dynamic_config, "certResolver: letsencrypt") - error_message = "The Keycloak router should request certificates through the Let's Encrypt resolver." - } - - assert { - condition = aws_route53_record.private.name == "id.glab.lol" - error_message = "The private DNS record should default to id.glab.lol." - } - - assert { - condition = local.acme_challenge_record_name == "_acme-challenge.id.acme.glab.lol" - error_message = "The default ACME challenge record should target the delegated Route 53 zone." - } - - assert { - condition = length(aws_vpc_security_group_ingress_rule.keycloak_https) == 1 - error_message = "The default lab CIDRs should produce exactly one HTTPS ingress rule." - } - - assert { - condition = length(aws_vpc_security_group_ingress_rule.operator_tailscale_https) == 2 - error_message = "The root module's operator_tailscale_cidrs should produce two HTTPS ingress rules." - } - - assert { - condition = alltrue([for rule in aws_vpc_security_group_ingress_rule.keycloak_https : rule.cidr_ipv4 != "0.0.0.0/0" && rule.from_port == 443 && rule.to_port == 443 && rule.ip_protocol == "tcp"]) - error_message = "Keycloak ingress should expose only HTTPS to non-public lab CIDRs." - } - - assert { - condition = module.github_token_broker.function_name == "glab-github-token-broker" - error_message = "The Keycloak stack should deploy the shared GitHub token broker name." - } - - assert { - condition = module.github_token_broker.deployed_version == "v2.0.0" - error_message = "The Keycloak stack should pin the current broker release." - } - - assert { - condition = aws_iam_role_policy.keycloak_github_token_broker_invoke.role == aws_iam_role.keycloak.id - error_message = "The Keycloak instance role should be allowed to invoke the token broker." - } - - assert { - condition = aws_iam_role_policy.keycloak_sops_decrypt.role == aws_iam_role.keycloak.id - error_message = "The Keycloak instance role should receive the SOPS KMS decrypt policy." - } -} - -run "plan_overrides" { - command = plan - - providers = { - aws = aws.mock - } - - variables { - data_volume_size = 32 - flatcar_ami_id = "ami-00000000000000000" - github_token_broker_function_name = "glab-keycloak-staging-github-token-broker" - instance_name = "glab-aws-keycloak-staging" - instance_type = "t4g.medium" - lab_cidrs = ["10.10.0.0/16", "10.20.0.0/16"] - operator_tailscale_cidrs = { - laptop = "100.64.1.1/32" - studio = "100.64.1.2/32" - } - private_hostname = "id.staging.glab.lol" - } - - assert { - condition = aws_instance.keycloak.ami == "ami-00000000000000000" - error_message = "The Flatcar AMI override should propagate." - } - - assert { - condition = aws_instance.keycloak.instance_type == "t4g.medium" - error_message = "The instance type override should propagate." - } - - assert { - condition = aws_ebs_volume.keycloak_data.size == 32 - error_message = "The data volume size override should propagate." - } - - assert { - condition = length(aws_vpc_security_group_ingress_rule.keycloak_https) == 2 - error_message = "Two lab CIDRs should produce two HTTPS ingress rules." - } - - assert { - condition = length(aws_vpc_security_group_ingress_rule.operator_tailscale_https) == 2 - error_message = "Two operator Tailscale CIDRs should produce two HTTPS ingress rules." - } - - assert { - condition = length(aws_route.operator_tailscale) == 2 - error_message = "Two operator Tailscale CIDRs should produce two return routes through the subnet router." - } - - assert { - condition = aws_instance.keycloak.tags["Name"] == "glab-aws-keycloak-staging" - error_message = "The instance Name tag should honor the instance_name override." - } - - assert { - condition = aws_route53_record.private.name == "id.staging.glab.lol" - error_message = "The private hostname override should propagate to DNS." - } - - assert { - condition = strcontains(local.lab_realm_config, "\"webAuthnPolicyRpId\": \"id.staging.glab.lol\"") - error_message = "The WebAuthn RP ID should follow the private hostname override." - } - - assert { - condition = local.acme_challenge_record_name == "_acme-challenge.id.staging.acme.glab.lol" - error_message = "The delegated ACME challenge record should follow the private hostname override." - } - - assert { - condition = module.github_token_broker.function_name == "glab-keycloak-staging-github-token-broker" - error_message = "The broker function name override should propagate." - } -} - -run "reject_invalid_lab_cidr" { - command = plan - - providers = { - aws = aws.mock - } - - variables { - lab_cidrs = ["not-a-cidr"] - } - - expect_failures = [ - var.lab_cidrs, - ] -} - -run "reject_invalid_operator_tailscale_cidr" { - command = plan - - providers = { - aws = aws.mock - } - - variables { - operator_tailscale_cidrs = { - laptop = "not-a-cidr" - } - } - - expect_failures = [ - var.operator_tailscale_cidrs, - ] -} - -run "reject_invalid_root_volume_size" { - command = plan - - providers = { - aws = aws.mock - } - - variables { - root_volume_size = 12 - } - - expect_failures = [ - var.root_volume_size, - ] -} - -run "reject_invalid_data_volume_size" { - command = plan - - providers = { - aws = aws.mock - } - - variables { - data_volume_size = 4 - } - - expect_failures = [ - var.data_volume_size, - ] -} - -run "reject_unpinned_labctl_image" { - command = plan - - providers = { - aws = aws.mock - } - - variables { - labctl_image = "ghcr.io/gilmanlab/platform/labctl:0.2.0" - } - - expect_failures = [ - var.labctl_image, - ] -} - -run "reject_non_keycloak_secret_path" { - command = plan - - providers = { - aws = aws.mock - } - - variables { - bootstrap_secret_path = "network/vyos/bootstrap.sops.yaml" - } - - expect_failures = [ - var.bootstrap_secret_path, - ] -} - -run "reject_non_keycloak_config_secret_path" { - command = plan - - providers = { - aws = aws.mock - } - - variables { - config_secret_path = "network/vyos/admin.sops.yaml" - } - - expect_failures = [ - var.config_secret_path, - ] -} - -run "reject_unpinned_keycloak_config_cli_image" { - command = plan - - providers = { - aws = aws.mock - } - - variables { - keycloak_config_cli_image = "quay.io/adorsys/keycloak-config-cli:latest-26" - } - - expect_failures = [ - var.keycloak_config_cli_image, - ] -} diff --git a/aws/keycloak/variables.tf b/aws/keycloak/variables.tf deleted file mode 100644 index eed96b6..0000000 --- a/aws/keycloak/variables.tf +++ /dev/null @@ -1,467 +0,0 @@ -variable "aws_region" { - description = "AWS region in which the Keycloak instance is created." - type = string - default = "us-west-2" -} - -variable "acme_ca_server" { - description = "ACME directory URL used by Traefik for Let's Encrypt certificate issuance." - type = string - default = "https://acme-v02.api.letsencrypt.org/directory" - - validation { - condition = startswith(var.acme_ca_server, "https://") - error_message = "acme_ca_server must be an HTTPS URL." - } -} - -variable "acme_email" { - description = "Email address used by Traefik when registering the Let's Encrypt ACME account." - type = string - default = "admin@glab.lol" - - validation { - condition = can(regex("^[^@[:space:]]+@[^@[:space:]]+$", var.acme_email)) - error_message = "acme_email must be an email address." - } -} - -variable "acme_zone_name" { - description = "Public Route 53 zone delegated from Cloudflare for ACME DNS-01 validation records." - type = string - default = "acme.glab.lol" - - validation { - condition = length(trimspace(var.acme_zone_name)) > 0 - error_message = "acme_zone_name must not be empty." - } -} - -variable "bootstrap_field" { - description = "RFC 6901 field path extracted from the SOPS file by labctl." - type = string - default = "/stack_env" - - validation { - condition = startswith(var.bootstrap_field, "/") - error_message = "bootstrap_field must be an RFC 6901 pointer beginning with '/'." - } -} - -variable "bootstrap_output_path" { - description = "Path where labctl writes the decrypted dotenv payload on the Flatcar host." - type = string - default = "/run/glab/keycloak/stack.env" - - validation { - condition = startswith(var.bootstrap_output_path, "/run/") - error_message = "bootstrap_output_path must stay under /run." - } -} - -variable "bootstrap_runtime_dir" { - description = "Ephemeral host directory mounted into the labctl container." - type = string - default = "/run/glab/keycloak" - - validation { - condition = startswith(var.bootstrap_runtime_dir, "/run/") && !endswith(var.bootstrap_runtime_dir, "/") - error_message = "bootstrap_runtime_dir must stay under /run and must not end with '/'." - } -} - -variable "bootstrap_secret_path" { - description = "Path to the SOPS-encrypted Keycloak bootstrap secret in GilmanLab/secrets." - type = string - default = "services/keycloak/bootstrap.sops.yaml" - - validation { - condition = can(regex("^services/keycloak/[^[:space:]]+\\.sops\\.yaml$", var.bootstrap_secret_path)) - error_message = "bootstrap_secret_path must point at a services/keycloak/*.sops.yaml file." - } -} - -variable "config_field" { - description = "RFC 6901 field path extracted from the SOPS file by labctl for Keycloak realm configuration." - type = string - default = "/admin_env" - - validation { - condition = startswith(var.config_field, "/") - error_message = "config_field must be an RFC 6901 pointer beginning with '/'." - } -} - -variable "config_output_path" { - description = "Path where labctl writes the decrypted Keycloak realm configuration dotenv payload." - type = string - default = "/run/glab/keycloak/admin.env" - - validation { - condition = startswith(var.config_output_path, "/run/") - error_message = "config_output_path must stay under /run." - } -} - -variable "config_secret_path" { - description = "Path to the SOPS-encrypted Keycloak realm configuration secret in GilmanLab/secrets." - type = string - default = "services/keycloak/admin.sops.yaml" - - validation { - condition = can(regex("^services/keycloak/[^[:space:]]+\\.sops\\.yaml$", var.config_secret_path)) - error_message = "config_secret_path must point at a services/keycloak/*.sops.yaml file." - } -} - -variable "data_dir" { - description = "Mount point for the encrypted Keycloak data volume." - type = string - default = "/var/lib/keycloak" - - validation { - condition = startswith(var.data_dir, "/") && !endswith(var.data_dir, "/") - error_message = "data_dir must be an absolute path and must not end with '/'." - } -} - -variable "data_volume_label" { - description = "Filesystem label assigned to the Keycloak data volume when first formatted." - type = string - default = "keycloak-data" - - validation { - condition = can(regex("^[A-Za-z0-9._-]{1,16}$", var.data_volume_label)) - error_message = "data_volume_label must be 1-16 filesystem-label-safe characters." - } -} - -variable "data_volume_size" { - description = "Size, in GiB, of the encrypted gp3 data volume mounted at data_dir." - type = number - default = 8 - - validation { - condition = var.data_volume_size >= 8 && var.data_volume_size <= 1024 - error_message = "data_volume_size must be between 8 and 1024 GiB." - } -} - -variable "data_volume_device_name" { - description = "Requested EC2 device name for the Keycloak data volume attachment." - type = string - default = "/dev/xvdf" - - validation { - condition = startswith(var.data_volume_device_name, "/dev/") - error_message = "data_volume_device_name must be an absolute /dev path." - } -} - -variable "dns_record_ttl" { - description = "TTL, in seconds, for the private Route 53 record pointing at the Keycloak instance." - type = number - default = 60 - - validation { - condition = var.dns_record_ttl >= 30 && var.dns_record_ttl <= 3600 - error_message = "dns_record_ttl must be between 30 and 3600 seconds." - } -} - -variable "flatcar_ami_id" { - description = "Flatcar stable arm64 AMI ID for us-west-2. Recheck the official Flatcar AWS EC2 table before live apply." - type = string - default = "ami-0ce605082061bbb10" - - validation { - condition = can(regex("^ami-[0-9a-f]{17}$", var.flatcar_ami_id)) - error_message = "flatcar_ami_id must be a literal EC2 AMI ID." - } -} - -variable "github_token_broker_function_name" { - description = "Name of the GitHub token broker Lambda deployed with the Keycloak stack." - type = string - default = "glab-github-token-broker" - - validation { - condition = can(regex("^[A-Za-z0-9_-]{1,64}$", var.github_token_broker_function_name)) - error_message = "github_token_broker_function_name must be 1-64 characters and contain only letters, numbers, hyphens, and underscores." - } -} - -variable "github_token_broker_log_retention_days" { - description = "CloudWatch log retention, in days, for the GitHub token broker Lambda log group." - type = number - default = 30 - - validation { - condition = contains( - [1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1827, 2192, 2557, 2922, 3288, 3653, 0], - var.github_token_broker_log_retention_days, - ) - error_message = "github_token_broker_log_retention_days must be one of the values accepted by CloudWatch Logs, or 0 for never expire." - } -} - -variable "github_token_broker_permissions" { - description = "GitHub App installation token permissions requested by the broker." - type = map(string) - default = { contents = "read" } - - validation { - condition = alltrue([ - for k, v in var.github_token_broker_permissions : length(trimspace(k)) > 0 && length(trimspace(v)) > 0 - ]) - error_message = "github_token_broker_permissions entries must have non-empty keys and values." - } -} - -variable "github_token_broker_private_key_kms_key_arn" { - description = "Optional customer-managed KMS key or alias ARN used by SSM to encrypt the GitHub App private key parameter." - type = string - default = null - - validation { - condition = ( - var.github_token_broker_private_key_kms_key_arn == null || - can(regex("^arn:aws[a-zA-Z-]*:kms:[a-z0-9-]+:[0-9]{12}:(key/[A-Za-z0-9-]+|alias/[A-Za-z0-9/_-]+)$", var.github_token_broker_private_key_kms_key_arn)) - ) - error_message = "github_token_broker_private_key_kms_key_arn must be a literal KMS key or alias ARN without wildcard characters." - } -} - -variable "github_token_broker_release_repository" { - description = "OWNER/REPO GitHub repository that publishes the GitHub token broker release asset." - type = string - default = "meigma/github-token-broker" - - validation { - condition = can(regex("^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$", var.github_token_broker_release_repository)) - error_message = "github_token_broker_release_repository must be a literal OWNER/REPO value." - } -} - -variable "github_token_broker_release_version" { - description = "Release tag of meigma/github-token-broker to deploy." - type = string - default = "v2.0.0" - - validation { - condition = can(regex("^v?[0-9]+\\.[0-9]+\\.[0-9]+(-[A-Za-z0-9.-]+)?$", var.github_token_broker_release_version)) - error_message = "github_token_broker_release_version must be a semver tag such as v2.0.0." - } -} - -variable "github_token_broker_ssm_parameter_paths" { - description = "SSM parameter paths holding the GitHub App credentials used by the broker." - type = object({ - client_id = string - installation_id = string - private_key = string - }) - default = { - client_id = "/glab/bootstrap/github-app/client-id" - installation_id = "/glab/bootstrap/github-app/installation-id" - private_key = "/glab/bootstrap/github-app/private-key-pem" - } - - validation { - condition = alltrue([ - can(regex("^/[A-Za-z0-9_.\\-/]+$", var.github_token_broker_ssm_parameter_paths.client_id)), - can(regex("^/[A-Za-z0-9_.\\-/]+$", var.github_token_broker_ssm_parameter_paths.installation_id)), - can(regex("^/[A-Za-z0-9_.\\-/]+$", var.github_token_broker_ssm_parameter_paths.private_key)), - ]) - error_message = "github_token_broker_ssm_parameter_paths entries must be absolute literal SSM paths." - } -} - -variable "iam_role_name" { - description = "IAM role name used by the Keycloak instance." - type = string - default = "glab-aws-keycloak" -} - -variable "instance_name" { - description = "Name tag for the Keycloak EC2 instance." - type = string - default = "glab-aws-keycloak" -} - -variable "instance_type" { - description = "EC2 instance type for the Keycloak host." - type = string - default = "t4g.small" -} - -variable "keycloak_image" { - description = "Pinned Keycloak container image." - type = string - default = "quay.io/keycloak/keycloak:26.6.1" -} - -variable "keycloak_config_cli_image" { - description = "Pinned keycloak-config-cli container image used for first-boot realm configuration." - type = string - default = "quay.io/adorsys/keycloak-config-cli@sha256:2d2a0663cf324379d9ffab896db8d00293cd0326151968b319cf166f6eec8fca" - - validation { - condition = startswith(var.keycloak_config_cli_image, "quay.io/adorsys/keycloak-config-cli@sha256:") - error_message = "keycloak_config_cli_image must be pinned by digest." - } -} - -variable "lab_cidrs" { - description = "CIDR blocks on the lab side that may reach Keycloak over HTTPS." - type = set(string) - default = ["10.10.0.0/16"] - - validation { - condition = alltrue([for cidr in var.lab_cidrs : can(cidrnetmask(cidr))]) - error_message = "Every entry in lab_cidrs must be a valid IPv4 CIDR block." - } -} - -variable "labctl_image" { - description = "Pinned labctl container image used for the bootstrap secret fetch." - type = string - default = "ghcr.io/gilmanlab/platform/labctl@sha256:4638b36a168df88d4206d5ff23aed62a6d8459ba7a2481c0b7c65c696445c1ec" - - validation { - condition = startswith(var.labctl_image, "ghcr.io/gilmanlab/platform/labctl@sha256:") - error_message = "labctl_image must be pinned by digest." - } -} - -variable "operator_tailscale_cidrs" { - description = "Named operator Tailscale IPv4 CIDRs that may reach Keycloak directly over HTTPS." - type = map(string) - default = {} - - validation { - condition = alltrue([for cidr in values(var.operator_tailscale_cidrs) : can(cidrnetmask(cidr))]) - error_message = "Every entry in operator_tailscale_cidrs must be a valid IPv4 CIDR block." - } -} - -variable "postgres_image" { - description = "Pinned Postgres container image." - type = string - default = "postgres:18.3-trixie" -} - -variable "private_hostname" { - description = "Private DNS hostname for the Keycloak service." - type = string - default = "id.glab.lol" - - validation { - condition = length(trimspace(var.private_hostname)) > 0 - error_message = "private_hostname must not be empty." - } -} - -variable "private_zone_name" { - description = "Private Route 53 zone that holds the Keycloak service record." - type = string - default = "glab.lol" - - validation { - condition = length(trimspace(var.private_zone_name)) > 0 - error_message = "private_zone_name must not be empty." - } -} - -variable "public_subnet_name" { - description = "Name tag used to discover the lab foundation public subnet." - type = string - default = "glab-lab-public" -} - -variable "public_route_table_name" { - description = "Name tag used to discover the lab foundation public route table." - type = string - default = "glab-lab-public" -} - -variable "root_volume_size" { - description = "Size, in GiB, of the encrypted gp3 root volume attached to the Keycloak instance." - type = number - default = 16 - - validation { - condition = var.root_volume_size >= 13 && var.root_volume_size <= 100 - error_message = "root_volume_size must be between 13 and 100 GiB for the current Flatcar AMI snapshot." - } -} - -variable "runtime_dir" { - description = "Root-backed host path that stores non-secret Keycloak and Traefik runtime config." - type = string - default = "/etc/glab/keycloak" - - validation { - condition = startswith(var.runtime_dir, "/") && !endswith(var.runtime_dir, "/") - error_message = "runtime_dir must be an absolute path and must not end with '/'." - } -} - -variable "security_group_name" { - description = "Name for the security group attached to the Keycloak instance." - type = string - default = "glab-aws-keycloak" -} - -variable "sops_kms_context_repo" { - description = "KMS encryption context Repo value allowed for Keycloak SOPS decrypts." - type = string - default = "GilmanLab/secrets" -} - -variable "sops_kms_context_scope" { - description = "KMS encryption context Scope value allowed for Keycloak SOPS decrypts." - type = string - default = "keycloak" -} - -variable "sops_kms_key_arn" { - description = "Customer-managed KMS key used by the secrets repository SOPS rules." - type = string - default = "arn:aws:kms:us-west-2:186067932323:key/2aba1d94-6eaf-4d80-8d26-2077f32fd7c5" - - validation { - condition = can(regex("^arn:aws[a-zA-Z-]*:kms:[a-z0-9-]+:[0-9]{12}:key/[A-Za-z0-9-]+$", var.sops_kms_key_arn)) - error_message = "sops_kms_key_arn must be a literal KMS key ARN." - } -} - -variable "subnet_router_instance_name" { - description = "Name tag used to discover the AWS subnet router instance for operator Tailscale return routes." - type = string - default = "glab-aws-subnet-router" -} - -variable "tags" { - description = "Extra tags to merge onto all created resources." - type = map(string) - default = {} -} - -variable "traefik_image" { - description = "Pinned Traefik container image." - type = string - default = "traefik:v3.6.13" -} - -variable "traefik_dns_challenge_resolvers" { - description = "Comma-separated public recursive resolvers Traefik uses while checking DNS-01 propagation." - type = string - default = "1.1.1.1:53,8.8.8.8:53" -} - -variable "vpc_name" { - description = "Name tag used to discover the lab foundation VPC." - type = string - default = "glab-lab-vpc" -} diff --git a/aws/lab-foundation/.gitignore b/aws/lab-foundation/.gitignore deleted file mode 100644 index eaecc08..0000000 --- a/aws/lab-foundation/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -.terraform/ -*.tfstate -*.tfstate.* -tfplan -crash.log -crash.*.log -override.tf -override.tf.json -*_override.tf -*_override.tf.json diff --git a/aws/lab-foundation/.terraform.lock.hcl b/aws/lab-foundation/.terraform.lock.hcl deleted file mode 100644 index b80adaa..0000000 --- a/aws/lab-foundation/.terraform.lock.hcl +++ /dev/null @@ -1,20 +0,0 @@ -# This file is maintained automatically by "tofu init". -# Manual edits may be lost in future updates. - -provider "registry.opentofu.org/hashicorp/aws" { - version = "5.100.0" - constraints = "~> 5.90" - hashes = [ - "h1:BrNG7eFOdRrRRbHdvrTjMJ8X8Oh/tiegURiKf7J2db8=", - "zh:1a41f3ee26720fee7a9a0a361890632a1701b5dc1cf5355dc651ddbe115682ff", - "zh:30457f36690c19307921885cc5e72b9dbeba369445815903acd5c39ac0e41e7a", - "zh:42c22674d5f23f6309eaf3ac3a4f1f8b66b566c1efe1dcb0dd2fb30c17ce1f78", - "zh:4cc271c795ff8ce6479ec2d11a8ba65a0a9ed6331def6693f4b9dccb6e662838", - "zh:60932aa376bb8c87cd1971240063d9d38ba6a55502c867fdbb9f5361dc93d003", - "zh:864e42784bde77b18393ebfcc0104cea9123da5f4392e8a059789e296952eefa", - "zh:9750423138bb01ecaa5cec1a6691664f7783d301fb1628d3b64a231b6b564e0e", - "zh:e5d30c4dec271ef9d6fe09f48237ec6cfea1036848f835b4e47f274b48bda5a7", - "zh:e62bd314ae97b43d782e0841b13e68a3f8ec85cc762004f973ce5ce7b6cdbfd0", - "zh:ea851a3c072528a4445ac6236ba2ce58ffc99ec466019b0bd0e4adde63a248e4", - ] -} diff --git a/aws/lab-foundation/Justfile b/aws/lab-foundation/Justfile deleted file mode 100644 index 4894a50..0000000 --- a/aws/lab-foundation/Justfile +++ /dev/null @@ -1,29 +0,0 @@ -set shell := ["bash", "-euo", "pipefail", "-c"] - -default: - @just --list - -# Offline validation: no backend or AWS credentials required. -check: - ./scripts/check.sh - -# Format Tofu files in place. -fmt: - tofu fmt -recursive - -# Initialize the working directory against the S3 backend. -init: - test -n "${GLAB_AWS_STATE_BUCKET:-}" || { echo "Set GLAB_AWS_STATE_BUCKET to the pre-created S3 backend bucket." >&2; exit 1; } - tofu init -reconfigure -backend-config="bucket=${GLAB_AWS_STATE_BUCKET}" - -# Render and save a plan to `tfplan`. -plan: - tofu plan -out=tfplan - -# Apply the saved plan produced by `just plan`. -apply: - tofu apply tfplan - -# Show outputs from the last applied state. -output: - tofu output diff --git a/aws/lab-foundation/README.md b/aws/lab-foundation/README.md deleted file mode 100644 index 1477794..0000000 --- a/aws/lab-foundation/README.md +++ /dev/null @@ -1,42 +0,0 @@ -# aws/lab-foundation - -OpenTofu stack for the first AWS-resident primitives in the `lab` account: - -- VPC (`172.16.0.0/16`) -- single public subnet -- internet gateway + public route table -- Route 53 private hosted zone for `glab.lol` -- Route 53 public hosted zone for delegated ACME DNS-01 validation -- customer-managed KMS key for SOPS - -This stack intentionally stops at the shared foundation layer. It does **not** -yet create EC2 instances, Tailscale, security groups, SSM bootstrap material, -or Keycloak-specific infrastructure. - -## Prerequisites - -- OpenTofu `>= 1.10` -- `just` -- `AWS_PROFILE` set to the `lab` account admin profile -- `GLAB_AWS_STATE_BUCKET` set to the pre-created S3 backend bucket in the - `lab` account - -The expected local operator flow is to export both values via `direnv`. - -## Usage - -```sh -just check -just init -just plan -just apply -``` - -`just init` uses `GLAB_AWS_STATE_BUCKET` to finish the otherwise-partial S3 -backend configuration. The backend bucket itself is part of the manual AWS -bootstrap and is intentionally not managed by this stack. - -After apply, delegate the `acme.glab.lol` public zone from Cloudflare using -the `acme_zone_name_servers` output. ACME clients should publish DNS-01 TXT -records in this delegated Route 53 zone instead of receiving Cloudflare API -credentials. diff --git a/aws/lab-foundation/backend.tf b/aws/lab-foundation/backend.tf deleted file mode 100644 index 7ecb354..0000000 --- a/aws/lab-foundation/backend.tf +++ /dev/null @@ -1,8 +0,0 @@ -terraform { - backend "s3" { - key = "aws/lab-foundation.tfstate" - region = "us-west-2" - encrypt = true - use_lockfile = true - } -} diff --git a/aws/lab-foundation/locals.tf b/aws/lab-foundation/locals.tf deleted file mode 100644 index 3001bf0..0000000 --- a/aws/lab-foundation/locals.tf +++ /dev/null @@ -1,9 +0,0 @@ -locals { - selected_availability_zone = var.availability_zone != "" ? var.availability_zone : data.aws_availability_zones.available.names[0] - - common_tags = merge(var.tags, { - "glab:project" = "glab" - "glab:domain" = "aws" - "glab:purpose" = "lab-foundation" - }) -} diff --git a/aws/lab-foundation/main.tf b/aws/lab-foundation/main.tf deleted file mode 100644 index 05bb563..0000000 --- a/aws/lab-foundation/main.tf +++ /dev/null @@ -1,90 +0,0 @@ -data "aws_availability_zones" "available" { - state = "available" -} - -data "aws_caller_identity" "current" {} - -resource "aws_vpc" "lab" { - cidr_block = var.vpc_cidr - enable_dns_support = true - enable_dns_hostnames = true - - tags = merge(local.common_tags, { - Name = "glab-lab-vpc" - }) -} - -resource "aws_internet_gateway" "lab" { - vpc_id = aws_vpc.lab.id - - tags = merge(local.common_tags, { - Name = "glab-lab-igw" - }) -} - -resource "aws_subnet" "public" { - vpc_id = aws_vpc.lab.id - cidr_block = var.public_subnet_cidr - availability_zone = local.selected_availability_zone - map_public_ip_on_launch = false - - tags = merge(local.common_tags, { - Name = "glab-lab-public" - }) -} - -resource "aws_route_table" "public" { - vpc_id = aws_vpc.lab.id - - tags = merge(local.common_tags, { - Name = "glab-lab-public" - }) -} - -resource "aws_route" "public_ipv4_default" { - route_table_id = aws_route_table.public.id - destination_cidr_block = "0.0.0.0/0" - gateway_id = aws_internet_gateway.lab.id -} - -resource "aws_route_table_association" "public" { - subnet_id = aws_subnet.public.id - route_table_id = aws_route_table.public.id -} - -resource "aws_route53_zone" "private" { - name = var.private_zone_name - comment = "Private hosted zone of record for the lab." - - tags = merge(local.common_tags, { - Name = var.private_zone_name - }) - - vpc { - vpc_id = aws_vpc.lab.id - } -} - -resource "aws_route53_zone" "acme" { - name = var.acme_zone_name - comment = "Public ACME DNS-01 validation zone for lab bootstrap services." - - tags = merge(local.common_tags, { - Name = var.acme_zone_name - }) -} - -resource "aws_kms_key" "sops" { - description = "SOPS recipient key for lab bootstrap and automation material." - deletion_window_in_days = var.kms_deletion_window_in_days - enable_key_rotation = true - - tags = merge(local.common_tags, { - Name = "glab-sops" - }) -} - -resource "aws_kms_alias" "sops" { - name = "alias/${var.kms_alias}" - target_key_id = aws_kms_key.sops.key_id -} diff --git a/aws/lab-foundation/moon.yml b/aws/lab-foundation/moon.yml deleted file mode 100644 index e7d8de1..0000000 --- a/aws/lab-foundation/moon.yml +++ /dev/null @@ -1,25 +0,0 @@ -layer: 'application' -tags: - - 'aws' - - 'tofu' - -project: - title: 'AWS lab foundation' - description: 'OpenTofu stack for the base network, DNS, and KMS primitives in the lab AWS account.' - owner: 'GilmanLab' - maintainers: - - 'josh' - -tasks: - check: - command: './scripts/check.sh' - toolchains: 'system' - inputs: - - '*.tf' - - '*.tfvars' - - '.terraform.lock.hcl' - - 'scripts/check.sh' - - 'tests/**/*.tftest.hcl' - options: - cache: false - runInCI: true diff --git a/aws/lab-foundation/outputs.tf b/aws/lab-foundation/outputs.tf deleted file mode 100644 index 777b753..0000000 --- a/aws/lab-foundation/outputs.tf +++ /dev/null @@ -1,64 +0,0 @@ -output "account_id" { - description = "AWS account ID where the lab foundation stack is applied." - value = data.aws_caller_identity.current.account_id -} - -output "availability_zone" { - description = "Availability zone selected for the public subnet." - value = local.selected_availability_zone -} - -output "vpc_id" { - description = "VPC ID for the lab foundation network." - value = aws_vpc.lab.id -} - -output "public_subnet_id" { - description = "Subnet ID of the single public subnet." - value = aws_subnet.public.id -} - -output "public_route_table_id" { - description = "Route table ID attached to the public subnet." - value = aws_route_table.public.id -} - -output "private_zone_id" { - description = "Route 53 hosted zone ID for the private lab zone." - value = aws_route53_zone.private.zone_id -} - -output "private_zone_name" { - description = "Route 53 hosted zone name for the private lab zone." - value = aws_route53_zone.private.name -} - -output "acme_zone_id" { - description = "Route 53 hosted zone ID for the public ACME validation zone." - value = aws_route53_zone.acme.zone_id -} - -output "acme_zone_name" { - description = "Route 53 hosted zone name for the public ACME validation zone." - value = aws_route53_zone.acme.name -} - -output "acme_zone_name_servers" { - description = "Nameservers to delegate from Cloudflare for the public ACME validation zone." - value = aws_route53_zone.acme.name_servers -} - -output "sops_kms_key_id" { - description = "KMS key ID for the SOPS recipient key." - value = aws_kms_key.sops.key_id -} - -output "sops_kms_key_arn" { - description = "KMS key ARN for the SOPS recipient key." - value = aws_kms_key.sops.arn -} - -output "sops_kms_alias" { - description = "KMS alias name for the SOPS recipient key." - value = aws_kms_alias.sops.name -} diff --git a/aws/lab-foundation/providers.tf b/aws/lab-foundation/providers.tf deleted file mode 100644 index 895583c..0000000 --- a/aws/lab-foundation/providers.tf +++ /dev/null @@ -1,10 +0,0 @@ -provider "aws" { - region = var.aws_region - - default_tags { - tags = { - "glab:managed-by" = "tofu" - "glab:stack" = "aws/lab-foundation" - } - } -} diff --git a/aws/lab-foundation/scripts/check.sh b/aws/lab-foundation/scripts/check.sh deleted file mode 100755 index f4403ce..0000000 --- a/aws/lab-foundation/scripts/check.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -TF_DATA_DIR="$(mktemp -d)" -trap 'rm -rf "$TF_DATA_DIR"' EXIT -export TF_DATA_DIR - -tofu fmt -check -recursive -tofu init -backend=false -input=false -tofu validate -tofu test diff --git a/aws/lab-foundation/terraform.tf b/aws/lab-foundation/terraform.tf deleted file mode 100644 index b7a66eb..0000000 --- a/aws/lab-foundation/terraform.tf +++ /dev/null @@ -1,10 +0,0 @@ -terraform { - required_version = ">= 1.10, < 2.0" - - required_providers { - aws = { - source = "hashicorp/aws" - version = "~> 5.90" - } - } -} diff --git a/aws/lab-foundation/tests/main.tftest.hcl b/aws/lab-foundation/tests/main.tftest.hcl deleted file mode 100644 index 471462d..0000000 --- a/aws/lab-foundation/tests/main.tftest.hcl +++ /dev/null @@ -1,97 +0,0 @@ -mock_provider "aws" { - alias = "mock" - - mock_data "aws_availability_zones" { - defaults = { - names = ["us-west-2a", "us-west-2b"] - } - } -} - -run "plan_defaults" { - command = plan - - providers = { - aws = aws.mock - } - - variables { - availability_zone = "us-west-2a" - } - - assert { - condition = aws_vpc.lab.cidr_block == "172.16.0.0/16" - error_message = "The default VPC CIDR should match the design doc." - } - - assert { - condition = aws_subnet.public.availability_zone == "us-west-2a" - error_message = "The public subnet should use the requested availability zone." - } - - assert { - condition = aws_route53_zone.private.name == "glab.lol" - error_message = "The private hosted zone should default to glab.lol." - } - - assert { - condition = aws_route53_zone.acme.name == "acme.glab.lol" - error_message = "The public ACME validation zone should default to acme.glab.lol." - } - - assert { - condition = aws_kms_alias.sops.name == "alias/glab-sops" - error_message = "The SOPS KMS alias should match the expected default." - } -} - -run "plan_overrides" { - command = plan - - providers = { - aws = aws.mock - } - - variables { - availability_zone = "us-west-2b" - acme_zone_name = "acme.corp.glab.lol" - kms_alias = "glab-bootstrap" - private_zone_name = "corp.glab.lol" - } - - assert { - condition = aws_subnet.public.availability_zone == "us-west-2b" - error_message = "The public subnet should honor the overridden availability zone." - } - - assert { - condition = aws_route53_zone.private.name == "corp.glab.lol" - error_message = "The private hosted zone should honor the overridden zone name." - } - - assert { - condition = aws_route53_zone.acme.name == "acme.corp.glab.lol" - error_message = "The public ACME validation zone should honor the overridden zone name." - } - - assert { - condition = aws_kms_alias.sops.name == "alias/glab-bootstrap" - error_message = "The SOPS KMS alias should honor the overridden alias." - } -} - -run "reject_invalid_vpc_cidr" { - command = plan - - providers = { - aws = aws.mock - } - - variables { - vpc_cidr = "not-a-cidr" - } - - expect_failures = [ - var.vpc_cidr, - ] -} diff --git a/aws/lab-foundation/variables.tf b/aws/lab-foundation/variables.tf deleted file mode 100644 index 7b51f29..0000000 --- a/aws/lab-foundation/variables.tf +++ /dev/null @@ -1,83 +0,0 @@ -variable "aws_region" { - description = "AWS region in which the lab foundation resources are created." - type = string - default = "us-west-2" -} - -variable "availability_zone" { - description = "Single AZ for the public subnet. Leave empty to use the first available AZ in the configured region." - type = string - default = "" -} - -variable "vpc_cidr" { - description = "CIDR block for the lab VPC." - type = string - default = "172.16.0.0/16" - - validation { - condition = can(cidrnetmask(var.vpc_cidr)) - error_message = "vpc_cidr must be a valid IPv4 CIDR block." - } -} - -variable "public_subnet_cidr" { - description = "CIDR block for the single public subnet in the lab VPC." - type = string - default = "172.16.0.0/24" - - validation { - condition = can(cidrnetmask(var.public_subnet_cidr)) - error_message = "public_subnet_cidr must be a valid IPv4 CIDR block." - } -} - -variable "private_zone_name" { - description = "Private Route 53 zone that acts as the lab DNS source of record." - type = string - default = "glab.lol" - - validation { - condition = length(trimspace(var.private_zone_name)) > 0 - error_message = "private_zone_name must not be empty." - } -} - -variable "acme_zone_name" { - description = "Public Route 53 zone delegated from Cloudflare for ACME DNS-01 validation records." - type = string - default = "acme.glab.lol" - - validation { - condition = length(trimspace(var.acme_zone_name)) > 0 - error_message = "acme_zone_name must not be empty." - } -} - -variable "kms_alias" { - description = "Alias for the customer-managed KMS key used as a SOPS recipient, without the 'alias/' prefix." - type = string - default = "glab-sops" - - validation { - condition = !startswith(var.kms_alias, "alias/") - error_message = "kms_alias must not include the 'alias/' prefix." - } -} - -variable "kms_deletion_window_in_days" { - description = "Pending deletion window, in days, when the KMS key is scheduled for deletion." - type = number - default = 30 - - validation { - condition = var.kms_deletion_window_in_days >= 7 && var.kms_deletion_window_in_days <= 30 - error_message = "kms_deletion_window_in_days must be between 7 and 30." - } -} - -variable "tags" { - description = "Extra tags to merge onto all created resources." - type = map(string) - default = {} -} diff --git a/aws/subnet-router/.gitignore b/aws/subnet-router/.gitignore deleted file mode 100644 index eaecc08..0000000 --- a/aws/subnet-router/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -.terraform/ -*.tfstate -*.tfstate.* -tfplan -crash.log -crash.*.log -override.tf -override.tf.json -*_override.tf -*_override.tf.json diff --git a/aws/subnet-router/.terraform.lock.hcl b/aws/subnet-router/.terraform.lock.hcl deleted file mode 100644 index b80adaa..0000000 --- a/aws/subnet-router/.terraform.lock.hcl +++ /dev/null @@ -1,20 +0,0 @@ -# This file is maintained automatically by "tofu init". -# Manual edits may be lost in future updates. - -provider "registry.opentofu.org/hashicorp/aws" { - version = "5.100.0" - constraints = "~> 5.90" - hashes = [ - "h1:BrNG7eFOdRrRRbHdvrTjMJ8X8Oh/tiegURiKf7J2db8=", - "zh:1a41f3ee26720fee7a9a0a361890632a1701b5dc1cf5355dc651ddbe115682ff", - "zh:30457f36690c19307921885cc5e72b9dbeba369445815903acd5c39ac0e41e7a", - "zh:42c22674d5f23f6309eaf3ac3a4f1f8b66b566c1efe1dcb0dd2fb30c17ce1f78", - "zh:4cc271c795ff8ce6479ec2d11a8ba65a0a9ed6331def6693f4b9dccb6e662838", - "zh:60932aa376bb8c87cd1971240063d9d38ba6a55502c867fdbb9f5361dc93d003", - "zh:864e42784bde77b18393ebfcc0104cea9123da5f4392e8a059789e296952eefa", - "zh:9750423138bb01ecaa5cec1a6691664f7783d301fb1628d3b64a231b6b564e0e", - "zh:e5d30c4dec271ef9d6fe09f48237ec6cfea1036848f835b4e47f274b48bda5a7", - "zh:e62bd314ae97b43d782e0841b13e68a3f8ec85cc762004f973ce5ce7b6cdbfd0", - "zh:ea851a3c072528a4445ac6236ba2ce58ffc99ec466019b0bd0e4adde63a248e4", - ] -} diff --git a/aws/subnet-router/Justfile b/aws/subnet-router/Justfile deleted file mode 100644 index 4894a50..0000000 --- a/aws/subnet-router/Justfile +++ /dev/null @@ -1,29 +0,0 @@ -set shell := ["bash", "-euo", "pipefail", "-c"] - -default: - @just --list - -# Offline validation: no backend or AWS credentials required. -check: - ./scripts/check.sh - -# Format Tofu files in place. -fmt: - tofu fmt -recursive - -# Initialize the working directory against the S3 backend. -init: - test -n "${GLAB_AWS_STATE_BUCKET:-}" || { echo "Set GLAB_AWS_STATE_BUCKET to the pre-created S3 backend bucket." >&2; exit 1; } - tofu init -reconfigure -backend-config="bucket=${GLAB_AWS_STATE_BUCKET}" - -# Render and save a plan to `tfplan`. -plan: - tofu plan -out=tfplan - -# Apply the saved plan produced by `just plan`. -apply: - tofu apply tfplan - -# Show outputs from the last applied state. -output: - tofu output diff --git a/aws/subnet-router/README.md b/aws/subnet-router/README.md deleted file mode 100644 index 9db644a..0000000 --- a/aws/subnet-router/README.md +++ /dev/null @@ -1,40 +0,0 @@ -# aws/subnet-router - -OpenTofu stack for the AWS-side subnet router in the `lab` account. - -This stack creates: - -- the EC2 instance profile and IAM role for Tailscale workload identity federation -- the EC2 subnet router instance -- the public Elastic IP -- the security group and VPC route entries needed to reach the lab CIDRs -- the SSM-driven host configuration that installs Docker and deploys `dns-mirror` - -This stack assumes the shared network primitives already exist in -`aws/lab-foundation`. - -## Prerequisites - -- OpenTofu `>= 1.10` -- `just` -- `AWS_PROFILE` set to the `lab` account admin profile -- `GLAB_AWS_STATE_BUCKET` set to the pre-created S3 backend bucket in the - `lab` account -- the Tailscale trust credential already created in `network/tailscale` - -The `tailscale_client_id` and `tailscale_audience` values are not secrets. They -come from the `network/tailscale` outputs and are committed in this root -module's `terraform.tfvars`. - -The `dns_mirror_image` value is intentionally pinned to an immutable semver tag -from the `platform` repo. The deployment wiring can be applied only after that -image exists in GHCR. - -## Usage - -```sh -just check -just init -just plan -just apply -``` diff --git a/aws/subnet-router/backend.tf b/aws/subnet-router/backend.tf deleted file mode 100644 index cca6665..0000000 --- a/aws/subnet-router/backend.tf +++ /dev/null @@ -1,8 +0,0 @@ -terraform { - backend "s3" { - key = "aws/subnet-router.tfstate" - region = "us-west-2" - encrypt = true - use_lockfile = true - } -} diff --git a/aws/subnet-router/compute.tf b/aws/subnet-router/compute.tf deleted file mode 100644 index 1f817e3..0000000 --- a/aws/subnet-router/compute.tf +++ /dev/null @@ -1,58 +0,0 @@ -resource "aws_instance" "subnet_router" { - ami = data.aws_ssm_parameter.al2023_arm64.value - iam_instance_profile = aws_iam_instance_profile.subnet_router.name - instance_type = var.instance_type - source_dest_check = false - subnet_id = data.aws_subnet.public.id - user_data = templatefile("${path.module}/templates/user_data.sh.tftpl", { - tailscale_advertise_routes_csv = local.tailscale_advertise_routes_csv - tailscale_audience = var.tailscale_audience - tailscale_client_id_with_params = "${var.tailscale_client_id}?ephemeral=false&preauthorized=true" - tailscale_hostname = var.instance_name - tailscale_tag = var.tailscale_tag - }) - user_data_replace_on_change = true - vpc_security_group_ids = [aws_security_group.subnet_router.id] - - tags = merge(local.common_tags, { - Name = var.instance_name - }) - - metadata_options { - http_endpoint = "enabled" - http_put_response_hop_limit = 2 - http_tokens = "required" - } - - root_block_device { - delete_on_termination = true - encrypted = true - volume_size = 8 - volume_type = "gp3" - } - - lifecycle { - ignore_changes = [ami] - } -} - -resource "aws_eip" "subnet_router" { - domain = "vpc" - - tags = merge(local.common_tags, { - Name = var.instance_name - }) -} - -resource "aws_eip_association" "subnet_router" { - allocation_id = aws_eip.subnet_router.id - network_interface_id = aws_instance.subnet_router.primary_network_interface_id -} - -resource "aws_route" "lab" { - for_each = var.lab_cidrs - - destination_cidr_block = each.value - network_interface_id = aws_instance.subnet_router.primary_network_interface_id - route_table_id = data.aws_route_table.public.id -} diff --git a/aws/subnet-router/data.tf b/aws/subnet-router/data.tf deleted file mode 100644 index 2633435..0000000 --- a/aws/subnet-router/data.tf +++ /dev/null @@ -1,78 +0,0 @@ -data "aws_iam_policy_document" "subnet_router_assume_role" { - statement { - actions = ["sts:AssumeRole"] - - principals { - identifiers = ["ec2.amazonaws.com"] - type = "Service" - } - } -} - -data "aws_iam_policy_document" "subnet_router_tailscale_outbound_federation" { - statement { - sid = "AllowGetWebIdentityTokenForTailscale" - actions = ["sts:GetWebIdentityToken"] - resources = ["*"] - - condition { - test = "ForAnyValue:StringEquals" - variable = "sts:IdentityTokenAudience" - values = [var.tailscale_audience] - } - - condition { - test = "NumericLessThanEquals" - variable = "sts:DurationSeconds" - values = ["300"] - } - } -} - -data "aws_iam_policy_document" "subnet_router_dns_mirror_route53" { - statement { - sid = "AllowReadMirroredHostedZone" - actions = [ - "route53:GetHostedZone", - "route53:ListResourceRecordSets", - ] - resources = [ - "arn:aws:route53:::hostedzone/${var.dns_mirror_hosted_zone_id}", - ] - } -} - -data "aws_route_table" "public" { - filter { - name = "tag:Name" - values = [var.public_route_table_name] - } - - filter { - name = "vpc-id" - values = [data.aws_vpc.lab.id] - } -} - -data "aws_ssm_parameter" "al2023_arm64" { - name = var.ami_ssm_parameter_name -} - -data "aws_subnet" "public" { - filter { - name = "tag:Name" - values = [var.public_subnet_name] - } - - filter { - name = "vpc-id" - values = [data.aws_vpc.lab.id] - } -} - -data "aws_vpc" "lab" { - filter { - name = "tag:Name" - values = [var.vpc_name] - } -} diff --git a/aws/subnet-router/deployment.tf b/aws/subnet-router/deployment.tf deleted file mode 100644 index c0d0661..0000000 --- a/aws/subnet-router/deployment.tf +++ /dev/null @@ -1,16 +0,0 @@ -resource "aws_ssm_association" "dns_mirror" { - name = "AWS-RunShellScript" - - parameters = { - commands = local.dns_mirror_bootstrap_script - } - - targets { - key = "InstanceIds" - values = [ - aws_instance.subnet_router.id, - ] - } - - wait_for_success_timeout_seconds = 600 -} diff --git a/aws/subnet-router/iam.tf b/aws/subnet-router/iam.tf deleted file mode 100644 index 7d89075..0000000 --- a/aws/subnet-router/iam.tf +++ /dev/null @@ -1,30 +0,0 @@ -resource "aws_iam_role" "subnet_router" { - assume_role_policy = data.aws_iam_policy_document.subnet_router_assume_role.json - name = var.iam_role_name - - tags = merge(local.common_tags, { - Name = var.iam_role_name - }) -} - -resource "aws_iam_role_policy" "subnet_router_tailscale_outbound_federation" { - name = "${var.iam_role_name}-tailscale-outbound-federation" - policy = data.aws_iam_policy_document.subnet_router_tailscale_outbound_federation.json - role = aws_iam_role.subnet_router.id -} - -resource "aws_iam_role_policy" "subnet_router_dns_mirror_route53" { - name = "${var.iam_role_name}-dns-mirror-route53" - policy = data.aws_iam_policy_document.subnet_router_dns_mirror_route53.json - role = aws_iam_role.subnet_router.id -} - -resource "aws_iam_role_policy_attachment" "subnet_router_ssm_managed_instance_core" { - policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore" - role = aws_iam_role.subnet_router.name -} - -resource "aws_iam_instance_profile" "subnet_router" { - name = var.iam_role_name - role = aws_iam_role.subnet_router.name -} diff --git a/aws/subnet-router/locals.tf b/aws/subnet-router/locals.tf deleted file mode 100644 index b0f0b59..0000000 --- a/aws/subnet-router/locals.tf +++ /dev/null @@ -1,27 +0,0 @@ -locals { - common_tags = { - "glab:project" = "glab" - "glab:domain" = "aws" - "glab:purpose" = "subnet-router" - } - - dns_mirror_compose = templatefile("${path.module}/templates/dns_mirror_compose.yml.tftpl", { - aws_region = var.aws_region - dns_mirror_hosted_zone_id = var.dns_mirror_hosted_zone_id - dns_mirror_image = var.dns_mirror_image - dns_mirror_listen_addr = var.dns_mirror_listen_addr - dns_mirror_output_path = var.dns_mirror_output_path - dns_mirror_state_dir = var.dns_mirror_state_dir - dns_mirror_sync_interval = var.dns_mirror_sync_interval - }) - dns_mirror_service_unit = templatefile("${path.module}/templates/dns_mirror.service.tftpl", { - dns_mirror_runtime_dir = var.dns_mirror_runtime_dir - }) - dns_mirror_bootstrap_script = templatefile("${path.module}/templates/dns_mirror_bootstrap.sh.tftpl", { - dns_mirror_compose = local.dns_mirror_compose - dns_mirror_runtime_dir = var.dns_mirror_runtime_dir - dns_mirror_service_unit = local.dns_mirror_service_unit - dns_mirror_state_dir = var.dns_mirror_state_dir - }) - tailscale_advertise_routes_csv = join(",", sort(tolist(var.tailscale_advertise_routes))) -} diff --git a/aws/subnet-router/moon.yml b/aws/subnet-router/moon.yml deleted file mode 100644 index d362661..0000000 --- a/aws/subnet-router/moon.yml +++ /dev/null @@ -1,25 +0,0 @@ -layer: 'application' -tags: - - 'aws' - - 'tofu' - -project: - title: 'AWS subnet router' - description: 'OpenTofu stack for the AWS EC2 subnet router that joins Tailscale through AWS workload identity federation.' - owner: 'GilmanLab' - maintainers: - - 'josh' - -tasks: - check: - command: './scripts/check.sh' - toolchains: 'system' - inputs: - - '*.tf' - - '*.tfvars' - - '.terraform.lock.hcl' - - 'scripts/check.sh' - - 'templates/**/*.tftpl' - options: - cache: false - runInCI: true diff --git a/aws/subnet-router/network.tf b/aws/subnet-router/network.tf deleted file mode 100644 index 5f11ccf..0000000 --- a/aws/subnet-router/network.tf +++ /dev/null @@ -1,41 +0,0 @@ -resource "aws_security_group" "subnet_router" { - description = "Security group for the AWS Tailscale subnet router." - name = var.security_group_name - vpc_id = data.aws_vpc.lab.id - - tags = merge(local.common_tags, { - Name = var.security_group_name - }) -} - -resource "aws_vpc_security_group_egress_rule" "subnet_router_ipv4" { - cidr_ipv4 = "0.0.0.0/0" - ip_protocol = "-1" - security_group_id = aws_security_group.subnet_router.id -} - -resource "aws_vpc_security_group_egress_rule" "subnet_router_ipv6" { - cidr_ipv6 = "::/0" - ip_protocol = "-1" - security_group_id = aws_security_group.subnet_router.id -} - -resource "aws_vpc_security_group_ingress_rule" "subnet_router_tailscale_udp" { - cidr_ipv4 = "0.0.0.0/0" - description = "Allow direct Tailscale WireGuard traffic." - from_port = 41641 - ip_protocol = "udp" - security_group_id = aws_security_group.subnet_router.id - to_port = 41641 -} - -resource "aws_vpc_security_group_ingress_rule" "subnet_router_lab" { - for_each = var.lab_cidrs - - cidr_ipv4 = each.value - description = "Allow lab-side traffic from ${each.value}." - from_port = -1 - ip_protocol = "-1" - security_group_id = aws_security_group.subnet_router.id - to_port = -1 -} diff --git a/aws/subnet-router/outputs.tf b/aws/subnet-router/outputs.tf deleted file mode 100644 index c4e4b3d..0000000 --- a/aws/subnet-router/outputs.tf +++ /dev/null @@ -1,29 +0,0 @@ -output "instance_id" { - description = "EC2 instance ID of the AWS subnet router." - value = aws_instance.subnet_router.id -} - -output "instance_private_ip" { - description = "Private IPv4 address of the AWS subnet router." - value = aws_instance.subnet_router.private_ip -} - -output "instance_role_arn" { - description = "IAM role ARN used by the AWS subnet router. This is the subject matched by the Tailscale federated identity." - value = aws_iam_role.subnet_router.arn -} - -output "network_interface_id" { - description = "Primary network interface ID of the AWS subnet router." - value = aws_instance.subnet_router.primary_network_interface_id -} - -output "public_ip" { - description = "Elastic IP attached to the AWS subnet router." - value = aws_eip.subnet_router.public_ip -} - -output "security_group_id" { - description = "Security group attached to the AWS subnet router." - value = aws_security_group.subnet_router.id -} diff --git a/aws/subnet-router/providers.tf b/aws/subnet-router/providers.tf deleted file mode 100644 index 4dbf1fa..0000000 --- a/aws/subnet-router/providers.tf +++ /dev/null @@ -1,10 +0,0 @@ -provider "aws" { - region = var.aws_region - - default_tags { - tags = { - "glab:managed-by" = "tofu" - "glab:stack" = "aws/subnet-router" - } - } -} diff --git a/aws/subnet-router/scripts/check.sh b/aws/subnet-router/scripts/check.sh deleted file mode 100755 index f4403ce..0000000 --- a/aws/subnet-router/scripts/check.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -TF_DATA_DIR="$(mktemp -d)" -trap 'rm -rf "$TF_DATA_DIR"' EXIT -export TF_DATA_DIR - -tofu fmt -check -recursive -tofu init -backend=false -input=false -tofu validate -tofu test diff --git a/aws/subnet-router/templates/dns_mirror.service.tftpl b/aws/subnet-router/templates/dns_mirror.service.tftpl deleted file mode 100644 index f441f70..0000000 --- a/aws/subnet-router/templates/dns_mirror.service.tftpl +++ /dev/null @@ -1,15 +0,0 @@ -[Unit] -Description=dns-mirror compose stack -Requires=docker.service -After=docker.service network-online.target - -[Service] -Type=oneshot -RemainAfterExit=yes -WorkingDirectory=${dns_mirror_runtime_dir} -ExecStart=/usr/bin/docker compose -f ${dns_mirror_runtime_dir}/compose.yml up -d --pull always -ExecStop=/usr/bin/docker compose -f ${dns_mirror_runtime_dir}/compose.yml down -TimeoutStartSec=0 - -[Install] -WantedBy=multi-user.target diff --git a/aws/subnet-router/templates/dns_mirror_bootstrap.sh.tftpl b/aws/subnet-router/templates/dns_mirror_bootstrap.sh.tftpl deleted file mode 100644 index 44e3767..0000000 --- a/aws/subnet-router/templates/dns_mirror_bootstrap.sh.tftpl +++ /dev/null @@ -1,37 +0,0 @@ -#!/bin/bash -set -euxo pipefail - -dnf install -y docker -systemctl enable --now docker - -compose_arch="$(uname -m)" -case "$${compose_arch}" in - x86_64) compose_arch="x86_64" ;; - aarch64) compose_arch="aarch64" ;; - *) - echo "unsupported architecture: $${compose_arch}" >&2 - exit 1 - ;; -esac - -install -d -m 0755 /usr/local/lib/docker/cli-plugins -curl -fsSL "https://github.com/docker/compose/releases/download/v2.38.2/docker-compose-linux-$${compose_arch}" \ - -o /usr/local/lib/docker/cli-plugins/docker-compose -chmod 0755 /usr/local/lib/docker/cli-plugins/docker-compose - -docker compose version >/dev/null 2>&1 - -install -d -m 0755 ${dns_mirror_runtime_dir} -install -d -m 0755 ${dns_mirror_state_dir} - -cat >${dns_mirror_runtime_dir}/compose.yml <<'EOF' -${dns_mirror_compose} -EOF - -cat >/etc/systemd/system/dns-mirror.service <<'EOF' -${dns_mirror_service_unit} -EOF - -systemctl daemon-reload -systemctl enable dns-mirror.service -systemctl restart dns-mirror.service diff --git a/aws/subnet-router/templates/dns_mirror_compose.yml.tftpl b/aws/subnet-router/templates/dns_mirror_compose.yml.tftpl deleted file mode 100644 index 54be1f5..0000000 --- a/aws/subnet-router/templates/dns_mirror_compose.yml.tftpl +++ /dev/null @@ -1,13 +0,0 @@ -services: - dns-mirror: - image: ${dns_mirror_image} - restart: unless-stopped - network_mode: host - environment: - AWS_REGION: ${aws_region} - DNS_MIRROR_HOSTED_ZONE_ID: ${dns_mirror_hosted_zone_id} - DNS_MIRROR_OUTPUT_PATH: ${dns_mirror_output_path} - DNS_MIRROR_SYNC_INTERVAL: ${dns_mirror_sync_interval} - DNS_MIRROR_LISTEN_ADDR: ${dns_mirror_listen_addr} - volumes: - - ${dns_mirror_state_dir}:${dns_mirror_state_dir} diff --git a/aws/subnet-router/templates/user_data.sh.tftpl b/aws/subnet-router/templates/user_data.sh.tftpl deleted file mode 100644 index d23e10d..0000000 --- a/aws/subnet-router/templates/user_data.sh.tftpl +++ /dev/null @@ -1,31 +0,0 @@ -#!/bin/bash -set -euxo pipefail - -cat >/etc/sysctl.d/99-tailscale.conf <<'EOF' -net.ipv4.ip_forward = 1 -net.ipv6.conf.all.forwarding = 1 -EOF - -sysctl -p /etc/sysctl.d/99-tailscale.conf - -curl -fsSL https://tailscale.com/install.sh | sh -systemctl enable --now tailscaled - -for attempt in $(seq 1 30); do - if tailscale up \ - --hostname='${tailscale_hostname}' \ - --advertise-tags='${tailscale_tag}' \ - --advertise-routes='${tailscale_advertise_routes_csv}' \ - --snat-subnet-routes=false \ - --accept-routes \ - --accept-dns=false \ - --client-id='${tailscale_client_id_with_params}' \ - --audience='${tailscale_audience}'; then - exit 0 - fi - - sleep 10 -done - -journalctl -u tailscaled --no-pager || true -exit 1 diff --git a/aws/subnet-router/terraform.tf b/aws/subnet-router/terraform.tf deleted file mode 100644 index b7a66eb..0000000 --- a/aws/subnet-router/terraform.tf +++ /dev/null @@ -1,10 +0,0 @@ -terraform { - required_version = ">= 1.10, < 2.0" - - required_providers { - aws = { - source = "hashicorp/aws" - version = "~> 5.90" - } - } -} diff --git a/aws/subnet-router/terraform.tfvars b/aws/subnet-router/terraform.tfvars deleted file mode 100644 index cc51206..0000000 --- a/aws/subnet-router/terraform.tfvars +++ /dev/null @@ -1,4 +0,0 @@ -aws_region = "us-west-2" -dns_mirror_image = "ghcr.io/gilmanlab/platform/services/dns-mirror:0.3.1" -tailscale_audience = "api.tailscale.com/THZctfF8wr11CNTRL-kzQFhHSFat11CNTRL" -tailscale_client_id = "THZctfF8wr11CNTRL-kzQFhHSFat11CNTRL" diff --git a/aws/subnet-router/tests/main.tftest.hcl b/aws/subnet-router/tests/main.tftest.hcl deleted file mode 100644 index 2d00a0f..0000000 --- a/aws/subnet-router/tests/main.tftest.hcl +++ /dev/null @@ -1,128 +0,0 @@ -mock_provider "aws" { - alias = "mock" - - mock_data "aws_ssm_parameter" { - defaults = { - value = "ami-0000000000000000" - } - } - - mock_data "aws_vpc" { - defaults = { - id = "vpc-00000000" - } - } - - mock_data "aws_subnet" { - defaults = { - id = "subnet-00000000" - } - } - - mock_data "aws_route_table" { - defaults = { - id = "rtb-00000000" - } - } - - mock_data "aws_iam_policy_document" { - defaults = { - json = "{\"Version\":\"2012-10-17\",\"Statement\":[]}" - } - } -} - -variables { - dns_mirror_image = "ghcr.io/gilmanlab/platform/services/dns-mirror:0.0.0-test" - tailscale_audience = "api.tailscale.com/test-audience" - tailscale_client_id = "test-client-id" -} - -run "plan_defaults" { - command = plan - - providers = { - aws = aws.mock - } - - assert { - condition = aws_instance.subnet_router.instance_type == "t4g.nano" - error_message = "The default instance type should match the design doc." - } - - assert { - condition = aws_iam_role.subnet_router.name == "glab-aws-subnet-router" - error_message = "The IAM role should default to glab-aws-subnet-router." - } - - assert { - condition = length(aws_route.lab) == 1 - error_message = "The default lab_cidrs should produce exactly one route." - } - - assert { - condition = local.tailscale_advertise_routes_csv == "172.16.0.0/16" - error_message = "The default advertised routes should produce the AWS VPC CIDR." - } -} - -run "plan_overrides" { - command = plan - - providers = { - aws = aws.mock - } - - variables { - instance_name = "glab-aws-subnet-router-staging" - instance_type = "t4g.small" - lab_cidrs = ["10.10.0.0/16", "10.20.0.0/16"] - } - - assert { - condition = aws_instance.subnet_router.instance_type == "t4g.small" - error_message = "The instance type override should propagate." - } - - assert { - condition = length(aws_route.lab) == 2 - error_message = "Two lab CIDRs should produce two routes." - } - - assert { - condition = aws_instance.subnet_router.tags["Name"] == "glab-aws-subnet-router-staging" - error_message = "The instance Name tag should honor the instance_name override." - } -} - -run "reject_invalid_lab_cidr" { - command = plan - - providers = { - aws = aws.mock - } - - variables { - lab_cidrs = ["not-a-cidr"] - } - - expect_failures = [ - var.lab_cidrs, - ] -} - -run "reject_invalid_tailscale_tag" { - command = plan - - providers = { - aws = aws.mock - } - - variables { - tailscale_tag = "subnet-router" - } - - expect_failures = [ - var.tailscale_tag, - ] -} diff --git a/aws/subnet-router/variables.tf b/aws/subnet-router/variables.tf deleted file mode 100644 index efccdce..0000000 --- a/aws/subnet-router/variables.tf +++ /dev/null @@ -1,137 +0,0 @@ -variable "ami_ssm_parameter_name" { - description = "SSM public parameter that resolves to the latest Amazon Linux 2023 arm64 AMI." - type = string - default = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64" -} - -variable "aws_region" { - description = "AWS region in which the subnet router is created." - type = string - default = "us-west-2" -} - -variable "dns_mirror_hosted_zone_id" { - description = "Route 53 hosted zone ID mirrored by dns-mirror." - type = string - default = "Z009084217D5KKVQERJY3" -} - -variable "dns_mirror_image" { - description = "Pinned GHCR image reference for the dns-mirror service." - type = string -} - -variable "dns_mirror_listen_addr" { - description = "HTTP listen address for the dns-mirror service." - type = string - default = ":8080" -} - -variable "dns_mirror_output_path" { - description = "On-host path where dns-mirror writes the rendered zonefile." - type = string - default = "/var/lib/dns-mirror/glab.lol.zone" -} - -variable "dns_mirror_runtime_dir" { - description = "Host path that stores the dns-mirror compose file." - type = string - default = "/opt/dns-mirror" -} - -variable "dns_mirror_state_dir" { - description = "Host path mounted into the dns-mirror container for persistent snapshots." - type = string - default = "/var/lib/dns-mirror" -} - -variable "dns_mirror_sync_interval" { - description = "Sync interval passed to the dns-mirror service." - type = string - default = "1m" -} - -variable "iam_role_name" { - description = "IAM role name used by the AWS subnet router instance." - type = string - default = "glab-aws-subnet-router" -} - -variable "instance_name" { - description = "Name tag for the AWS subnet router instance." - type = string - default = "glab-aws-subnet-router" -} - -variable "instance_type" { - description = "EC2 instance type for the AWS subnet router." - type = string - default = "t4g.nano" -} - -variable "lab_cidrs" { - description = "CIDR blocks on the lab side that should be routed to the AWS subnet router and allowed through its security group." - type = set(string) - default = ["10.10.0.0/16"] - - validation { - condition = alltrue([for cidr in var.lab_cidrs : can(cidrnetmask(cidr))]) - error_message = "Every entry in lab_cidrs must be a valid IPv4 CIDR block." - } -} - -variable "public_route_table_name" { - description = "Name tag used to discover the lab foundation public route table." - type = string - default = "glab-lab-public" -} - -variable "public_subnet_name" { - description = "Name tag used to discover the lab foundation public subnet." - type = string - default = "glab-lab-public" -} - -variable "security_group_name" { - description = "Name for the security group attached to the AWS subnet router." - type = string - default = "glab-aws-subnet-router" -} - -variable "tailscale_advertise_routes" { - description = "CIDR blocks the AWS subnet router advertises to the tailnet." - type = set(string) - default = ["172.16.0.0/16"] - - validation { - condition = alltrue([for cidr in var.tailscale_advertise_routes : can(cidrnetmask(cidr))]) - error_message = "Every entry in tailscale_advertise_routes must be a valid IPv4 CIDR block." - } -} - -variable "tailscale_audience" { - description = "Audience configured on the Tailscale federated identity for the AWS subnet router." - type = string -} - -variable "tailscale_client_id" { - description = "Client ID configured on the Tailscale federated identity for the AWS subnet router." - type = string -} - -variable "tailscale_tag" { - description = "Tailnet tag the AWS subnet router advertises when it joins through workload identity federation." - type = string - default = "tag:subnet-router" - - validation { - condition = startswith(var.tailscale_tag, "tag:") - error_message = "tailscale_tag must start with 'tag:'." - } -} - -variable "vpc_name" { - description = "Name tag used to discover the lab foundation VPC." - type = string - default = "glab-lab-vpc" -} diff --git a/network/tailscale/.gitignore b/network/tailscale/.gitignore deleted file mode 100644 index b892d53..0000000 --- a/network/tailscale/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -.terraform/ -tfplan -*.tfstate -*.tfstate.backup diff --git a/network/tailscale/.terraform.lock.hcl b/network/tailscale/.terraform.lock.hcl deleted file mode 100644 index 1ab9d78..0000000 --- a/network/tailscale/.terraform.lock.hcl +++ /dev/null @@ -1,24 +0,0 @@ -# This file is maintained automatically by "tofu init". -# Manual edits may be lost in future updates. - -provider "registry.opentofu.org/tailscale/tailscale" { - version = "0.28.0" - constraints = "~> 0.28" - hashes = [ - "h1:Cquej+BY4u2GJ90N1l7CCwP4aukIYHUvnlgQIkpLFbU=", - "zh:26dc44b865b055069fbada06d8fcf0d44c32a365823219db1c122458c377a2f0", - "zh:335d4d4e6293647ff1b9dd828aef14e8b3cdf6ffae22e63084308a0b051d5e23", - "zh:40e78f96ac9af15060a0b0fb2eee8d51b61b8d160f063e75c3af7b855d85367e", - "zh:410d0d11cf2abbb6b8a7117d833a4326b52ba0a0eb9717e623f3703bc6c6ebc1", - "zh:49b90c658eabfb26559704dcc4a4da3bc1895f1724b8ea80d031ee22826e9426", - "zh:511d87ece288524ed3601c636838f041dcb4bf5aa7cef532677b65767ae0dda3", - "zh:55cedf481b4a357f625ed785f685e46e3087955bd66750eeaf631836e992a6f4", - "zh:5f58fcbe3980e64e3423f851cf2b7efad06df7382a813b21c4c3d7ce6c41b378", - "zh:a831d89d7b0b8e9d609c68e026d2beb8aa2173831de0baee6739745e94b70aa6", - "zh:a91ee9291198b358e1e57772e55d3e5bd882d676be0238a21d39f26980d26a5d", - "zh:bbf1c42927b62bb787bf624dc6d2089b422e3fe28bb02f27d10da1b184904279", - "zh:e6c2116f2d9140e7d1487ab48f5fa55e53e8251018155ee59d867b54f8713a4d", - "zh:ebfd3caf5feda0b5251a9ba62be54456a257b4d1ac237d9cccee6487a3ba7581", - "zh:f39ceae695cac365708e9f0e866801322723c00a1447c398d6eba376817afbcf", - ] -} diff --git a/network/tailscale/Justfile b/network/tailscale/Justfile deleted file mode 100644 index c255e39..0000000 --- a/network/tailscale/Justfile +++ /dev/null @@ -1,41 +0,0 @@ -set shell := ["bash", "-euo", "pipefail", "-c"] - -aws_vault_profile := "jmgilman-prod" -secrets_file := "network/tailscale/terraform.sops.yaml" - -default: - @just --list - -# Offline validation: no backend or credentials required. -check: - tofu fmt -check -recursive - tofu init -backend=false -input=false - tofu validate - -# Format Tofu files in place. -fmt: - tofu fmt -recursive - -# Initialize the Tofu working directory against the S3 backend. -init: - GLAB_SECRETS_DIR="${GLAB_SECRETS_DIR:-../../../secrets}"; \ - export TF_VAR_tailscale_tailnet="$(sops -d --extract '["tailnet"]' "$GLAB_SECRETS_DIR/{{secrets_file}}")"; \ - export TF_VAR_tailscale_oauth_client_id="$(sops -d --extract '["oauth_client_id"]' "$GLAB_SECRETS_DIR/{{secrets_file}}")"; \ - export TF_VAR_tailscale_oauth_client_secret="$(sops -d --extract '["oauth_client_secret"]' "$GLAB_SECRETS_DIR/{{secrets_file}}")"; \ - aws-vault exec {{aws_vault_profile}} -- tofu init - -# Render and save a plan to `tfplan`. -plan: - GLAB_SECRETS_DIR="${GLAB_SECRETS_DIR:-../../../secrets}"; \ - export TF_VAR_tailscale_tailnet="$(sops -d --extract '["tailnet"]' "$GLAB_SECRETS_DIR/{{secrets_file}}")"; \ - export TF_VAR_tailscale_oauth_client_id="$(sops -d --extract '["oauth_client_id"]' "$GLAB_SECRETS_DIR/{{secrets_file}}")"; \ - export TF_VAR_tailscale_oauth_client_secret="$(sops -d --extract '["oauth_client_secret"]' "$GLAB_SECRETS_DIR/{{secrets_file}}")"; \ - aws-vault exec {{aws_vault_profile}} -- tofu plan -out=tfplan - -# Apply the saved plan produced by `just plan`. -apply: - GLAB_SECRETS_DIR="${GLAB_SECRETS_DIR:-../../../secrets}"; \ - export TF_VAR_tailscale_tailnet="$(sops -d --extract '["tailnet"]' "$GLAB_SECRETS_DIR/{{secrets_file}}")"; \ - export TF_VAR_tailscale_oauth_client_id="$(sops -d --extract '["oauth_client_id"]' "$GLAB_SECRETS_DIR/{{secrets_file}}")"; \ - export TF_VAR_tailscale_oauth_client_secret="$(sops -d --extract '["oauth_client_secret"]' "$GLAB_SECRETS_DIR/{{secrets_file}}")"; \ - aws-vault exec {{aws_vault_profile}} -- tofu apply tfplan diff --git a/network/tailscale/README.md b/network/tailscale/README.md deleted file mode 100644 index 43c1e68..0000000 --- a/network/tailscale/README.md +++ /dev/null @@ -1,21 +0,0 @@ -# Tailscale - -This stack manages tailnet settings that are not part of the Tailscale ACL -policy file. - -It currently: - -- keeps MagicDNS enabled -- points the lab split DNS zones at the VyOS recursor -- manages the AWS subnet router federated identity used for workload identity - federation from the `lab` AWS account - -- `lab.gilman.io` -- `10.10.10.in-addr.arpa` -- `70.10.10.in-addr.arpa` - -Credentials come from the private `secrets` repo at -`network/tailscale/terraform.sops.yaml`. - -The OAuth client in that file must be permitted to manage both DNS settings and -trust credentials in the tailnet. diff --git a/network/tailscale/backend.tf b/network/tailscale/backend.tf deleted file mode 100644 index aada9fa..0000000 --- a/network/tailscale/backend.tf +++ /dev/null @@ -1,9 +0,0 @@ -terraform { - backend "s3" { - bucket = "gilmanlab-tfstate" - key = "network/tailscale.tfstate" - region = "us-west-2" - encrypt = true - use_lockfile = true - } -} diff --git a/network/tailscale/main.tf b/network/tailscale/main.tf deleted file mode 100644 index 49f2e64..0000000 --- a/network/tailscale/main.tf +++ /dev/null @@ -1,49 +0,0 @@ -locals { - aws_subnet_router_scopes = toset([ - "auth_keys", - ]) - - aws_subnet_router_routes = toset([ - "172.16.0.0/16", - ]) - - lab_split_dns_domains = toset([ - "glab.lol", - "lab.gilman.io", - "10.10.10.in-addr.arpa", - "70.10.10.in-addr.arpa", - ]) - - lab_dns_nameservers = [ - "10.10.10.1", - ] -} - -resource "tailscale_dns_preferences" "magic_dns" { - magic_dns = true -} - -resource "tailscale_dns_split_nameservers" "lab" { - for_each = local.lab_split_dns_domains - - domain = each.value - nameservers = local.lab_dns_nameservers -} - -resource "tailscale_federated_identity" "aws_subnet_router" { - description = "glab AWS subnet router" - issuer = var.aws_subnet_router_issuer - scopes = local.aws_subnet_router_scopes - subject = var.aws_subnet_router_subject - tags = [var.aws_subnet_router_tag] -} - -data "tailscale_device" "aws_subnet_router" { - hostname = "glab-aws-subnet-router" - wait_for = "120s" -} - -resource "tailscale_device_subnet_routes" "aws_subnet_router" { - device_id = data.tailscale_device.aws_subnet_router.node_id - routes = local.aws_subnet_router_routes -} diff --git a/network/tailscale/outputs.tf b/network/tailscale/outputs.tf deleted file mode 100644 index f88ee87..0000000 --- a/network/tailscale/outputs.tf +++ /dev/null @@ -1,9 +0,0 @@ -output "aws_subnet_router_audience" { - description = "Audience that the AWS subnet router must request when exchanging its AWS-issued OIDC token with Tailscale." - value = tailscale_federated_identity.aws_subnet_router.audience -} - -output "aws_subnet_router_client_id" { - description = "Client ID that the AWS subnet router passes to tailscale up when using workload identity federation." - value = tailscale_federated_identity.aws_subnet_router.id -} diff --git a/network/tailscale/providers.tf b/network/tailscale/providers.tf deleted file mode 100644 index 74fcc63..0000000 --- a/network/tailscale/providers.tf +++ /dev/null @@ -1,16 +0,0 @@ -terraform { - required_version = ">= 1.10" - - required_providers { - tailscale = { - source = "tailscale/tailscale" - version = "~> 0.28" - } - } -} - -provider "tailscale" { - tailnet = var.tailscale_tailnet - oauth_client_id = var.tailscale_oauth_client_id - oauth_client_secret = var.tailscale_oauth_client_secret -} diff --git a/network/tailscale/terraform.tfvars b/network/tailscale/terraform.tfvars deleted file mode 100644 index 2718416..0000000 --- a/network/tailscale/terraform.tfvars +++ /dev/null @@ -1,2 +0,0 @@ -aws_subnet_router_issuer = "https://a1caa8f5-e117-4c0c-9bec-e083d0afedbb.tokens.sts.global.api.aws" -aws_subnet_router_subject = "arn:aws:iam::186067932323:role/glab-aws-subnet-router" diff --git a/network/tailscale/variables.tf b/network/tailscale/variables.tf deleted file mode 100644 index 0bfa24b..0000000 --- a/network/tailscale/variables.tf +++ /dev/null @@ -1,37 +0,0 @@ -variable "aws_subnet_router_issuer" { - description = "Account-specific AWS STS OIDC issuer URL used by the lab subnet router workload identity federation trust." - type = string -} - -variable "aws_subnet_router_subject" { - description = "Exact AWS principal ARN that the tailnet trust configuration accepts for the AWS subnet router." - type = string -} - -variable "aws_subnet_router_tag" { - description = "Tailnet tag granted to the AWS subnet router when it registers through workload identity federation." - type = string - default = "tag:subnet-router" - - validation { - condition = startswith(var.aws_subnet_router_tag, "tag:") - error_message = "aws_subnet_router_tag must start with 'tag:'." - } -} - -variable "tailscale_tailnet" { - description = "Tailscale tailnet name or organization ID." - type = string -} - -variable "tailscale_oauth_client_id" { - description = "OAuth client ID with write access to the Tailscale DNS and trust-credential APIs." - type = string - sensitive = true -} - -variable "tailscale_oauth_client_secret" { - description = "OAuth client secret with write access to the Tailscale DNS and trust-credential APIs." - type = string - sensitive = true -} diff --git a/security/pki/root-ca/.gitignore b/security/pki/root-ca/.gitignore deleted file mode 100644 index eaecc08..0000000 --- a/security/pki/root-ca/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -.terraform/ -*.tfstate -*.tfstate.* -tfplan -crash.log -crash.*.log -override.tf -override.tf.json -*_override.tf -*_override.tf.json diff --git a/security/pki/root-ca/.terraform.lock.hcl b/security/pki/root-ca/.terraform.lock.hcl deleted file mode 100644 index b80adaa..0000000 --- a/security/pki/root-ca/.terraform.lock.hcl +++ /dev/null @@ -1,20 +0,0 @@ -# This file is maintained automatically by "tofu init". -# Manual edits may be lost in future updates. - -provider "registry.opentofu.org/hashicorp/aws" { - version = "5.100.0" - constraints = "~> 5.90" - hashes = [ - "h1:BrNG7eFOdRrRRbHdvrTjMJ8X8Oh/tiegURiKf7J2db8=", - "zh:1a41f3ee26720fee7a9a0a361890632a1701b5dc1cf5355dc651ddbe115682ff", - "zh:30457f36690c19307921885cc5e72b9dbeba369445815903acd5c39ac0e41e7a", - "zh:42c22674d5f23f6309eaf3ac3a4f1f8b66b566c1efe1dcb0dd2fb30c17ce1f78", - "zh:4cc271c795ff8ce6479ec2d11a8ba65a0a9ed6331def6693f4b9dccb6e662838", - "zh:60932aa376bb8c87cd1971240063d9d38ba6a55502c867fdbb9f5361dc93d003", - "zh:864e42784bde77b18393ebfcc0104cea9123da5f4392e8a059789e296952eefa", - "zh:9750423138bb01ecaa5cec1a6691664f7783d301fb1628d3b64a231b6b564e0e", - "zh:e5d30c4dec271ef9d6fe09f48237ec6cfea1036848f835b4e47f274b48bda5a7", - "zh:e62bd314ae97b43d782e0841b13e68a3f8ec85cc762004f973ce5ce7b6cdbfd0", - "zh:ea851a3c072528a4445ac6236ba2ce58ffc99ec466019b0bd0e4adde63a248e4", - ] -} diff --git a/security/pki/root-ca/Justfile b/security/pki/root-ca/Justfile deleted file mode 100644 index 930139e..0000000 --- a/security/pki/root-ca/Justfile +++ /dev/null @@ -1,36 +0,0 @@ -set shell := ["bash", "-euo", "pipefail", "-c"] - -default: - @just --list - -# Initialize the Tofu working directory against the S3 backend. -init: - test -n "${GLAB_AWS_STATE_BUCKET:-}" || { echo "Set GLAB_AWS_STATE_BUCKET to the pre-created S3 backend bucket." >&2; exit 1; } - tofu init -reconfigure -backend-config="bucket=${GLAB_AWS_STATE_BUCKET}" - -# Offline validation: no backend, no credentials required. -check: - tofu fmt -check -recursive - tofu init -backend=false -input=false - tofu validate - -# Format Tofu files in place. -fmt: - tofu fmt -recursive - -# Render and save a plan to `tfplan`. -plan: - tofu plan -out=tfplan - -# Apply the saved plan produced by `just plan`. -apply: - tofu apply tfplan - -# Show outputs from the last applied state. -output: - tofu output - -# Schedule the root CA key for deletion. Irreversible after the window elapses. -[confirm("Destroy the root CA KMS key? AWS schedules it for deletion using the configured window.")] -destroy: - tofu destroy diff --git a/security/pki/root-ca/README.md b/security/pki/root-ca/README.md deleted file mode 100644 index 602a196..0000000 --- a/security/pki/root-ca/README.md +++ /dev/null @@ -1,86 +0,0 @@ -# root-ca - -Offline-by-policy root CA KMS key for the glab internal PKI. - -The signing key is generated inside AWS KMS in the current `lab` account and -never exported. This stack manages only the KMS key and alias. Root -certificate minting is an explicit operator action after the key exists. - -## Prerequisites - -- `tofu` >= 1.10 -- `aws` CLI credentials for the `lab` account -- `just` -- `step` CLI with the `step-kms-plugin` binary on `PATH` for certificate - minting - -The expected local operator flow is to export these from the workspace -`.envrc`: - -```sh -export AWS_PROFILE=lab-admin -export GLAB_AWS_STATE_BUCKET=glab-lab-tfstate-186067932323 -``` - -## Key - -```sh -just check -just init -just plan -just apply -``` - -`just init` uses `GLAB_AWS_STATE_BUCKET` to configure the otherwise-partial S3 -backend. The backend bucket is part of the manual AWS bootstrap and is not -managed by this stack. - -Run `just` with no arguments for the full recipe list. - -## Certificate - -The committed `root_ca.crt` is the self-signed trust anchor issued by the KMS -key. `root_ca.fingerprint` contains the SHA-256 fingerprint used by clients -that need an out-of-band trust bootstrap value. - -The root uses `pathlen:2` so the future hierarchy remains open: - -```text -Root CA pathlen:2 - -> cluster Vault intermediate pathlen:1 - -> SPIRE intermediate pathlen:0 - -> workload SVID leaves -``` - -Use the checked-in `templates/root-ca.tpl` template when re-minting this -certificate. The built-in `step certificate create --profile root-ca` profile -produces the older `pathlen:1` shape. - -```sh -KEY_ID=$(aws kms describe-key \ - --key-id alias/glab-pki-root-ca \ - --region us-west-2 \ - --query KeyMetadata.KeyId \ - --output text) - -step certificate create 'glab Root CA' root_ca.crt \ - --template templates/root-ca.tpl \ - --not-after 175200h \ - --kms 'awskms:region=us-west-2' \ - --key "awskms:key-id=${KEY_ID}" \ - --force - -step certificate fingerprint root_ca.crt > root_ca.fingerprint -``` - -## Notes - -- Target account: `186067932323` (`lab`) -- Region: `us-west-2` -- Alias: `alias/glab-pki-root-ca` -- Key spec: `ECC_NIST_P384`, `SIGN_VERIFY`, 30-day deletion window -- Root validity: 20 years -- State: `s3://glab-lab-tfstate-186067932323/security/pki/root-ca.tfstate`, - encrypted with native S3 locking -- Key policy is the AWS default; signing rights should be granted per use, not - held by always-on lab workloads diff --git a/security/pki/root-ca/backend.tf b/security/pki/root-ca/backend.tf deleted file mode 100644 index 067fa57..0000000 --- a/security/pki/root-ca/backend.tf +++ /dev/null @@ -1,8 +0,0 @@ -terraform { - backend "s3" { - key = "security/pki/root-ca.tfstate" - region = "us-west-2" - encrypt = true - use_lockfile = true - } -} diff --git a/security/pki/root-ca/main.tf b/security/pki/root-ca/main.tf deleted file mode 100644 index 4ccd99e..0000000 --- a/security/pki/root-ca/main.tf +++ /dev/null @@ -1,22 +0,0 @@ -locals { - common_tags = merge(var.tags, { - "glab:project" = "glab" - "glab:domain" = "security/pki" - "glab:purpose" = "root-ca" - }) -} - -resource "aws_kms_key" "root_ca" { - description = "Root CA signing key for the glab internal PKI. Used only for intermediate issuance and rotation." - customer_master_key_spec = var.key_spec - key_usage = "SIGN_VERIFY" - deletion_window_in_days = var.deletion_window_in_days - multi_region = false - - tags = local.common_tags -} - -resource "aws_kms_alias" "root_ca" { - name = "alias/${var.key_alias}" - target_key_id = aws_kms_key.root_ca.key_id -} diff --git a/security/pki/root-ca/outputs.tf b/security/pki/root-ca/outputs.tf deleted file mode 100644 index a749101..0000000 --- a/security/pki/root-ca/outputs.tf +++ /dev/null @@ -1,19 +0,0 @@ -output "key_id" { - description = "KMS key ID of the root CA." - value = aws_kms_key.root_ca.key_id -} - -output "key_arn" { - description = "KMS key ARN of the root CA." - value = aws_kms_key.root_ca.arn -} - -output "key_alias" { - description = "KMS alias name for the root CA key." - value = aws_kms_alias.root_ca.name -} - -output "key_spec" { - description = "Customer master key spec of the root CA key." - value = aws_kms_key.root_ca.customer_master_key_spec -} diff --git a/security/pki/root-ca/providers.tf b/security/pki/root-ca/providers.tf deleted file mode 100644 index 2ae9c5c..0000000 --- a/security/pki/root-ca/providers.tf +++ /dev/null @@ -1,22 +0,0 @@ -terraform { - required_version = ">= 1.10" - - required_providers { - aws = { - source = "hashicorp/aws" - version = "~> 5.90" - } - } -} - -provider "aws" { - allowed_account_ids = [var.aws_account_id] - region = var.aws_region - - default_tags { - tags = { - "glab:managed-by" = "tofu" - "glab:stack" = "security/pki/root-ca" - } - } -} diff --git a/security/pki/root-ca/root_ca.crt b/security/pki/root-ca/root_ca.crt deleted file mode 100644 index 20fc5e0..0000000 --- a/security/pki/root-ca/root_ca.crt +++ /dev/null @@ -1,12 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIBrzCCATWgAwIBAgIRAN0TATVtK8TU3bBHKc/3mGwwCgYIKoZIzj0EAwMwFzEV -MBMGA1UEAxMMZ2xhYiBSb290IENBMB4XDTI2MDQyMTIwMjExMloXDTQ2MDQxNjIw -MjExMVowFzEVMBMGA1UEAxMMZ2xhYiBSb290IENBMHYwEAYHKoZIzj0CAQYFK4EE -ACIDYgAEHmlLUKipoFUsA+UwNiby58vW/ath2s+cxKpy1yZd1OQFW+jr9HX3HrOP -5JPVD4KY8SYnCEkgvU8f2LofbBgEpSY0ZyZyMBFdEz5k0DRIOE7y6lLx2sifvCVt -CzXnHq7Zo0UwQzAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBAjAd -BgNVHQ4EFgQU1czozOgIVsQW5a/4D7GTFIcV94gwCgYIKoZIzj0EAwMDaAAwZQIw -SJy/PI9HzXBf+7+bdeLqwE68U/XhVCfzMHPsmnmjOS5KQO7Cfbbh23cKPyZWE3UO -AjEA6Aqln8SLKWjrLibEdqCVRO3cqPzIpY67wWxf4IUr5RSLPGAmkm8OzmNe4oa1 -1HM3 ------END CERTIFICATE----- diff --git a/security/pki/root-ca/root_ca.fingerprint b/security/pki/root-ca/root_ca.fingerprint deleted file mode 100644 index f7bb2e8..0000000 --- a/security/pki/root-ca/root_ca.fingerprint +++ /dev/null @@ -1 +0,0 @@ -a9a9e7bddd21de7f6162d3c4b6b4dd76dc1901f4f713620a277688046d1217ac diff --git a/security/pki/root-ca/templates/root-ca.tpl b/security/pki/root-ca/templates/root-ca.tpl deleted file mode 100644 index 9cd24b7..0000000 --- a/security/pki/root-ca/templates/root-ca.tpl +++ /dev/null @@ -1,9 +0,0 @@ -{ - "subject": {{ toJson .Subject }}, - "issuer": {{ toJson .Subject }}, - "keyUsage": ["certSign", "crlSign"], - "basicConstraints": { - "isCA": true, - "maxPathLen": 2 - } -} diff --git a/security/pki/root-ca/terraform.tfvars b/security/pki/root-ca/terraform.tfvars deleted file mode 100644 index d078194..0000000 --- a/security/pki/root-ca/terraform.tfvars +++ /dev/null @@ -1 +0,0 @@ -aws_region = "us-west-2" diff --git a/security/pki/root-ca/variables.tf b/security/pki/root-ca/variables.tf deleted file mode 100644 index 50fabb0..0000000 --- a/security/pki/root-ca/variables.tf +++ /dev/null @@ -1,34 +0,0 @@ -variable "aws_account_id" { - description = "AWS account ID where the root CA KMS key is created." - type = string - default = "186067932323" -} - -variable "aws_region" { - description = "AWS region in which the root CA KMS key is created." - type = string -} - -variable "key_alias" { - description = "KMS alias for the root CA key, without the 'alias/' prefix." - type = string - default = "glab-pki-root-ca" -} - -variable "key_spec" { - description = "KMS customer_master_key_spec for the root CA key. Must be a SIGN_VERIFY-capable asymmetric key spec." - type = string - default = "ECC_NIST_P384" -} - -variable "deletion_window_in_days" { - description = "Pending deletion window, in days, when the key is scheduled for deletion. AWS allows 7-30; this stack defaults to the safest maximum." - type = number - default = 30 -} - -variable "tags" { - description = "Extra tags to merge onto all created resources." - type = map(string) - default = {} -}