Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions aws/github-token-broker/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# 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:
Expand Down
20 changes: 19 additions & 1 deletion aws/keycloak/.terraform.lock.hcl

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 37 additions & 0 deletions aws/keycloak/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ This stack creates:
- 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
- an SSM-driven Docker Compose deployment for Postgres, Keycloak, and Traefik

This stack intentionally stops at starting Keycloak. It does **not** configure
Expand All @@ -29,13 +32,47 @@ the delegated TXT record for this hostname.
- `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`

The expected local operator flow is to export both 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
Expand Down
39 changes: 39 additions & 0 deletions aws/keycloak/github_token_broker.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
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
}
20 changes: 20 additions & 0 deletions aws/keycloak/outputs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,26 @@ output "iam_role_arn" {
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
Expand Down
46 changes: 43 additions & 3 deletions aws/keycloak/tests/main.tftest.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,31 @@ mock_provider "aws" {
}
}

mock_data "aws_partition" {
defaults = {
partition = "aws"
}
}

mock_data "aws_region" {
defaults = {
name = "us-west-2"
}
}

mock_data "aws_iam_policy_document" {
defaults = {
json = "{\"Version\":\"2012-10-17\",\"Statement\":[]}"
}
}

mock_resource "aws_iam_role" {
defaults = {
arn = "arn:aws:iam::123456789012:role/mock-role"
id = "mock-role"
}
}

mock_data "aws_route53_zone" {
defaults = {
zone_id = "Z00000000000000000"
Expand Down Expand Up @@ -122,6 +141,21 @@ run "plan_defaults" {
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."
}
}

run "plan_overrides" {
Expand All @@ -132,9 +166,10 @@ run "plan_overrides" {
}

variables {
instance_name = "glab-aws-keycloak-staging"
instance_type = "t4g.medium"
lab_cidrs = ["10.10.0.0/16", "10.20.0.0/16"]
instance_name = "glab-aws-keycloak-staging"
instance_type = "t4g.medium"
github_token_broker_function_name = "glab-keycloak-staging-github-token-broker"
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"
Expand Down Expand Up @@ -176,6 +211,11 @@ run "plan_overrides" {
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" {
Expand Down
97 changes: 97 additions & 0 deletions aws/keycloak/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,103 @@ variable "instance_type" {
default = "t4g.small"
}

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 "keycloak_admin_username" {
description = "Temporary bootstrap admin username passed to Keycloak on first startup."
type = string
Expand Down
Loading