Wire Forge-native release infrastructure
Some checks failed
Cache: Publish Nix / Publish Nix Cache (push) Waiting to run
Build Rust / Cargo Test (push) Successful in 4m14s
Build Site / Next.js Build (push) Failing after 6s
Infra: OpenTofu / OpenTofu (grafana) (push) Successful in 5s
Lint Governance / BEP Metadata (push) Successful in 0s
Build: Android / Android Rust Core Stub (push) Failing after 23s

This commit is contained in:
Conrad Kramer 2026-06-07 05:51:12 -07:00
parent 97c569fb35
commit 002bd382e9
199 changed files with 14268 additions and 185 deletions

4
infra/grafana/.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
.terraform/
backend.hcl
terraform.tfvars
*.tfplan

29
infra/grafana/.terraform.lock.hcl generated Normal file
View file

@ -0,0 +1,29 @@
# This file is maintained automatically by "tofu init".
# Manual edits may be lost in future updates.
provider "registry.opentofu.org/grafana/grafana" {
version = "4.37.0"
constraints = ">= 4.36.0, < 5.0.0"
hashes = [
"h1:EqimatXUHgE9jIko0ifoR+kOLVCQlLGvj13iH5GoCnE=",
"zh:02dcb17aae3e97a4b946e28267a80845b130e4a062ebd0f7dbf15ef1f65713e7",
"zh:07b57580fe411de152a3b29a5ac1e0ff2541f9a62904c5b9703cf7e3c63bbbfa",
"zh:089b7ce58b919732bedcb95e2a5d99553956a3637e89bb0b814de82050cca635",
"zh:08c15290aafeec6945cd5321b5cdc2adb5f077b26fb77225984e3acd055293f7",
"zh:0e0d0f89d8f2ca89541b5981c9388263af15c83c98f57d831a2dc162f1586af5",
"zh:3363c8c5a987951ec6bf69bfdd1293d2fb1e72aa94e272280b69ef2dd3988e34",
"zh:479e2f82ad2b7522851c828f33d6ab7a0cfce44304f25b5b1f5ddabed0fb879c",
"zh:4d9d619b6c6c619f58624a9fba8414201fe68403eb5e1588b95d8c31ecc9c71e",
"zh:545f24ef5b1d87d641a577556ada8b0ac67417e0bc51ac01238898fb6429d67a",
"zh:8b43993924d12d946e002acbf54c8bf7f05a269436ad085c45317c6020ba10df",
"zh:965cf4bf12a8a8e858d791743716433d71a17ed2a15e6cd183dc3d7349be24e3",
"zh:9a1205bc5cf01f143611afb97934f2cbc5c9225f6bc5c279f6424ea4da96151b",
"zh:b49d28a1a0226799a9421b50dcdc2c85de5858d42f92c8443745d80c9dd5e0cd",
"zh:b618aa9e7272447b2e1ce46bb399e39d21f605de48c756b6af0b42fddca50674",
"zh:b9dac24ca39fcc95575f0051390c71dabdd540ea2c365bb87b5621e5d6219594",
"zh:ba3b79a13ece3bd782a79afe590200940ce740eee809ed4cb9e1086253dba1d2",
"zh:ba3f77877ed139cbf0d9af9d1b84e827c49fc42c60e545c67c9d05d1e63e9b0c",
"zh:ba84824517ef8bb78122f7bdcbd49bbdf289df9b82040dfbee913a73f29fc158",
"zh:de116fd3a62a20f183793c760fa4d9521cee7d718165924cc9b0012e297636b5",
]
}

46
infra/grafana/README.md Normal file
View file

@ -0,0 +1,46 @@
# Burrow Grafana API Control Plane
This OpenTofu stack creates Grafana folders and dashboards through the official
`grafana/grafana` provider.
This is not a Grafana deployment mechanism. NixOS owns the Grafana service,
authentication, data sources, and boot-time provisioning.
## Run
Use the repository wrapper so the OpenTofu runtime comes from Nix:
```sh
Scripts/grafana-tofu.sh init -backend-config=backend.hcl
Scripts/grafana-tofu.sh plan -var-file=terraform.tfvars
```
For a local syntax check without remote state:
```sh
Scripts/grafana-tofu.sh init -backend=false
Scripts/grafana-tofu.sh validate
```
Manual Forgejo runs use `.forgejo/workflows/infra-opentofu.yml`. Plan, apply,
and import modes need `GRAFANA_AUTH` or a decrypted admin password so the
Grafana provider can reach the API.
## Adoption
The default stack manages the checked-in dashboards:
```text
services/grafana/dashboards/burrow-overview.json
services/grafana/dashboards/headscale.json
services/grafana/dashboards/observability.json
```
Import existing production objects before enabling apply:
```sh
Scripts/grafana-tofu.sh import 'grafana_folder.folders["burrow"]' 'burrow'
Scripts/grafana-tofu.sh import 'grafana_dashboard.dashboards["burrow-overview"]' 'burrow-overview'
Scripts/grafana-tofu.sh import 'grafana_dashboard.dashboards["burrow-headscale"]' 'burrow-headscale'
Scripts/grafana-tofu.sh import 'grafana_dashboard.dashboards["burrow-observability"]' 'burrow-observability'
```

View file

@ -0,0 +1,5 @@
bucket = "burrow-opentofu-state"
key = "grafana/grafana.tfstate"
region = "us-west-2"
dynamodb_table = "burrow-opentofu-locks"
encrypt = true

17
infra/grafana/grafana.tf Normal file
View file

@ -0,0 +1,17 @@
resource "grafana_folder" "folders" {
for_each = var.manage_grafana_dashboards ? var.grafana_folders : {}
title = each.value.title
uid = each.value.uid
}
resource "grafana_dashboard" "dashboards" {
for_each = var.manage_grafana_dashboards ? var.grafana_dashboards : {}
config_json = file(startswith(each.value.path, "/") ? each.value.path : "${path.module}/${each.value.path}")
folder = each.value.folder
message = each.value.message
overwrite = each.value.overwrite
depends_on = [grafana_folder.folders]
}

9
infra/grafana/outputs.tf Normal file
View file

@ -0,0 +1,9 @@
output "grafana_dashboard_urls" {
description = "Grafana dashboard URLs managed by this stack."
value = { for name, dashboard in grafana_dashboard.dashboards : name => dashboard.url }
}
output "grafana_folder_uids" {
description = "Grafana folder UIDs managed by this stack."
value = { for name, folder in grafana_folder.folders : name => folder.uid }
}

View file

@ -0,0 +1,4 @@
provider "grafana" {
url = var.grafana_url
auth = var.grafana_auth
}

View file

@ -0,0 +1,18 @@
grafana_url = "https://graphs.burrow.net"
manage_grafana_dashboards = true
grafana_folders = {
burrow = {
title = "Burrow"
uid = "burrow"
}
}
grafana_dashboards = {
burrow-overview = {
path = "../../services/grafana/dashboards/burrow-overview.json"
folder = "burrow"
overwrite = true
}
}

View file

@ -0,0 +1,56 @@
variable "grafana_url" {
description = "Grafana base URL."
type = string
default = "https://graphs.burrow.net"
}
variable "grafana_auth" {
description = "Grafana API token or basic auth in username:password form. Prefer GRAFANA_AUTH in CI."
type = string
default = null
sensitive = true
}
variable "manage_grafana_dashboards" {
description = "Manage Grafana folders and dashboards through the Grafana API."
type = bool
default = false
}
variable "grafana_folders" {
description = "Grafana folders to create before dashboards."
type = map(object({
title = string
uid = optional(string)
}))
default = {
burrow = {
title = "Burrow"
uid = "burrow"
}
}
}
variable "grafana_dashboards" {
description = "Grafana dashboards to manage from checked-in JSON."
type = map(object({
path = string
folder = optional(string)
message = optional(string, "Managed by Burrow OpenTofu")
overwrite = optional(bool, true)
}))
default = {
burrow-overview = {
path = "../../services/grafana/dashboards/burrow-overview.json"
folder = "burrow"
}
burrow-headscale = {
path = "../../services/grafana/dashboards/headscale.json"
folder = "burrow"
}
burrow-observability = {
path = "../../services/grafana/dashboards/observability.json"
folder = "burrow"
}
}
}

12
infra/grafana/versions.tf Normal file
View file

@ -0,0 +1,12 @@
terraform {
required_version = ">= 1.8.0"
required_providers {
grafana = {
source = "grafana/grafana"
version = ">= 4.36.0, < 5.0"
}
}
backend "s3" {}
}

5
infra/identity/.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
.terraform/
backend.hcl
terraform.tfstate
terraform.tfstate.backup
*.tfplan

20
infra/identity/.terraform.lock.hcl generated Normal file
View file

@ -0,0 +1,20 @@
# This file is maintained automatically by "tofu init".
# Manual edits may be lost in future updates.
provider "registry.opentofu.org/hashicorp/google" {
version = "6.50.0"
constraints = ">= 5.40.0, < 7.0.0"
hashes = [
"h1:MAAe4zFFdqS9M5rpmJK/vKgdb6ZMD/s/0Xd97yTDipA=",
"zh:1d4695f807d998f11fcdcfa174766287b82a8093513af857bcdad2d81c642480",
"zh:3173ac5df0294624d113812e49e2a55714aff7db617488168cecdf4168df9e29",
"zh:34d2b3d44c23bd6354fc4ab5917b302872ea1ab8de107034567f955b1717fa5b",
"zh:3a77f3cc2f3664cd5aaeeef4d044e6ec1695a079588fffec3ca03953664e5f04",
"zh:6b444e4b629ea8dc8cb112a39dde098dc5584d26d6de4177558f556a9a226696",
"zh:96545c8cd4d3a57069c5d1799eab5aedd887e16d98b5559a195f6d2c2d9bc674",
"zh:ba464caafde95ee16671d6b5ec90f053ed77a9d06c567456db6efd9160fa3165",
"zh:d876938e5b0d3f57a984d9be72467995f87fef6569968623415dc51d9f54d30b",
"zh:dfd908d873e314ab807d0abc9cfd42d2611cd06dc1b9ec719ebdbb738e8e68d6",
"zh:f9f16819a7738d564afd45fd169ba61004ec4e4e7089d2a4950cb8895be1fe1f",
]
}

145
infra/identity/README.md Normal file
View file

@ -0,0 +1,145 @@
# Burrow Identity KMS and WIF
This OpenTofu stack owns Google Cloud KMS and Workload Identity Federation
resources for Burrow identity and release automation.
It may manage:
- the `burrow-identity` Google KMS key ring
- non-exportable KMS signing keys for Authentik, the SAML CA, and release
signing
- dedicated non-exportable KMS signing keys for APT, RPM, pacman, Flatpak, and
AUR source release surfaces
- a non-exportable KMS signing key for the Apple Developer ID Application CSR
path (`apple-developer-id-application`)
- a non-exportable KMS signing key for the Apple iOS Distribution CSR path
(`apple-ios-distribution`)
- a non-exportable software KMS signing key for Sparkle Ed25519 appcast
signatures (`sparkle-ed25519`)
- an Authentik-backed WIF pool/provider for Forgejo runners
- the runner service account and its narrow IAM bindings for the
`burrow-automation` group and the Authentik WIF client ID
`google-wif.burrow.net`
It must not manage:
- Authentik private signing key material
- OpenBao bootstrap tokens
- store credentials or service-account JSON keys
Forgejo CI should authenticate with `Scripts/ci/google-wif-auth.sh`. That helper
uses Authentik to obtain an OIDC token and lets Google WIF impersonate
`burrow-forgejo-runner@project-88c23ce9-918a-470a-b33.iam.gserviceaccount.com`
in project `project-88c23ce9-918a-470a-b33`.
## Apple Developer ID CSR
`Scripts/apple/google-kms-csr.py` builds a standard PKCS#10 CSR using the public
key from `projects/project-88c23ce9-918a-470a-b33/locations/global/keyRings/burrow-identity/cryptoKeys/apple-developer-id-application`
and asks Google KMS to sign the CSR payload. The private key remains
non-exportable in KMS.
Forgejo can generate the Apple-uploadable CSR with:
```sh
Scripts/forgejo-dispatch.sh apple-developer-id-kms-csr.yml --ref main
```
Apple currently documents Developer ID certificate creation through the
Developer website or Xcode. After downloading the returned `.cer`, keep it with
release artifacts; do not convert this key path into an exportable `.p12`.
The active certificate created from this path is Apple certificate
`9JKN6HXBHC`, stored at
`Apple/Certificates/developer-id-application-9JKN6HXBHC.cer`. Its public key
matches KMS key version `apple-developer-id-application/cryptoKeyVersions/1`.
## Apple iOS Distribution CSR
The iOS App Store distribution certificate uses the same CSR builder with the
`apple-ios-distribution` KMS key:
```sh
Scripts/apple/google-kms-csr.py \
--kms-key apple-ios-distribution \
--common-name "Burrow iOS Distribution" \
--output ios-distribution.certSigningRequest
```
The App Store Connect API can create the certificate from that CSR:
```sh
Scripts/apple/create-asc-certificate.mjs \
--certificate-type IOS_DISTRIBUTION \
--csr-file ios-distribution.certSigningRequest \
--out-dir Apple/Certificates
```
The active certificate created from this path is Apple certificate
`3G42677598`, stored at `Apple/Certificates/ios-distribution-3G42677598.cer`.
Its public key matches KMS key version
`apple-ios-distribution/cryptoKeyVersions/1`.
## Sparkle Appcast Signing
Sparkle appcasts use `sparkle-ed25519`, a non-exportable Google KMS key with
algorithm `EC_SIGN_ED25519` and software protection level. Google KMS rejects
Ed25519 at HSM protection level, so this key is intentionally separate from the
HSM RSA keys used for Apple certificate CSRs and repository signing.
The public EdDSA key embedded in macOS release builds is:
```text
Myv9ZNZT6YGKMtMezh52ra4WqaeEKc4VlvVU0evhJeI=
```
The appcast signer is:
```sh
Scripts/sparkle/sign-appcast-kms.py \
--appcast dist/builds/<build>/sparkle/<channel>/appcast.xml \
--artifact-dir dist/builds/<build>/sparkle/<channel>
```
## Run
```sh
Scripts/identity-tofu.sh init -backend-config=backend.hcl
Scripts/identity-tofu.sh plan -var-file=terraform.tfvars
```
For local syntax work without remote state:
```sh
Scripts/identity-tofu.sh init -backend=false
Scripts/identity-tofu.sh validate
```
Start with `manage_google = false`. Import existing resources before enabling
management, then turn on one resource group at a time.
## Bootstrap Imports
The first live Apple Developer ID pass may need these resources imported before
an `identity` apply can own them:
```sh
Scripts/identity-tofu.sh import 'google_kms_key_ring.identity[0]' \
projects/project-88c23ce9-918a-470a-b33/locations/global/keyRings/burrow-identity
Scripts/identity-tofu.sh import 'google_kms_crypto_key.signing["apple_developer_id_application"]' \
projects/project-88c23ce9-918a-470a-b33/locations/global/keyRings/burrow-identity/cryptoKeys/apple-developer-id-application
Scripts/identity-tofu.sh import 'google_kms_crypto_key.signing["apple_ios_distribution"]' \
projects/project-88c23ce9-918a-470a-b33/locations/global/keyRings/burrow-identity/cryptoKeys/apple-ios-distribution
Scripts/identity-tofu.sh import 'google_kms_crypto_key.signing["sparkle_ed25519"]' \
projects/project-88c23ce9-918a-470a-b33/locations/global/keyRings/burrow-identity/cryptoKeys/sparkle-ed25519
Scripts/identity-tofu.sh import 'google_service_account.runner[0]' \
projects/project-88c23ce9-918a-470a-b33/serviceAccounts/burrow-forgejo-runner@project-88c23ce9-918a-470a-b33.iam.gserviceaccount.com
```
Run imports with the same `manage_google`, key-ring, and service-account
variables that will be used for apply. Key-level IAM bindings should be imported
after the signing keys and service account exist.

View file

@ -0,0 +1,4 @@
bucket = "burrow-opentofu-state"
key = "identity/identity.tfstate"
region = "us-west-2"
encrypt = true

173
infra/identity/google.tf Normal file
View file

@ -0,0 +1,173 @@
locals {
google_required_services = toset([
"cloudkms.googleapis.com",
"cloudresourcemanager.googleapis.com",
"iam.googleapis.com",
"iamcredentials.googleapis.com",
"sts.googleapis.com",
])
google_existing_kms_key_ring_id = var.google_kms_key_ring_id != "" ? var.google_kms_key_ring_id : "projects/${var.google_project_id}/locations/${var.google_kms_key_ring_location}/keyRings/${var.google_kms_key_ring_name}"
google_kms_key_ring_id = var.manage_google_kms_key_ring ? (
google_kms_key_ring.identity[0].id
) : local.google_existing_kms_key_ring_id
google_runner_service_account_email = var.manage_google_runner_service_account ? (
google_service_account.runner[0].email
) : var.google_runner_service_account_email
google_runner_wif_group_members = var.google_wif_runner_group != "" ? (
toset([
"principalSet://iam.googleapis.com/projects/${var.google_project_number}/locations/global/workloadIdentityPools/${var.google_wif_pool_id}/attribute.burrow_groups/${var.google_wif_runner_group}"
])
) : (
toset([])
)
google_runner_wif_client_members = var.google_wif_runner_client_id != "" ? (
toset([
"principalSet://iam.googleapis.com/projects/${var.google_project_number}/locations/global/workloadIdentityPools/${var.google_wif_pool_id}/attribute.burrow_client_id/${var.google_wif_runner_client_id}"
])
) : (
toset([])
)
google_runner_wif_members = setunion(
local.google_runner_wif_group_members,
local.google_runner_wif_client_members,
var.google_wif_runner_group == "" && var.google_wif_runner_client_id == "" ? toset([
"principalSet://iam.googleapis.com/projects/${var.google_project_number}/locations/global/workloadIdentityPools/${var.google_wif_pool_id}/*"
]) : toset([])
)
google_signer_members = setunion(
var.google_release_signer_members,
toset(["serviceAccount:${local.google_runner_service_account_email}"]),
)
google_signer_grants = var.manage_google ? {
for grant in flatten([
for key_name in keys(google_kms_crypto_key.signing) : [
for member in local.google_signer_members : {
id = "${key_name}|${member}"
key_name = key_name
member = member
}
]
]) : grant.id => grant
} : {}
}
resource "google_project_service" "identity" {
for_each = var.manage_google ? local.google_required_services : toset([])
project = var.google_project_id
service = each.value
disable_on_destroy = false
}
resource "google_kms_key_ring" "identity" {
count = var.manage_google && var.manage_google_kms_key_ring ? 1 : 0
project = var.google_project_id
location = var.google_kms_key_ring_location
name = var.google_kms_key_ring_name
depends_on = [google_project_service.identity]
}
resource "google_kms_crypto_key" "signing" {
for_each = var.manage_google && local.google_kms_key_ring_id != "" ? var.google_signing_keys : {}
name = each.value.name
key_ring = local.google_kms_key_ring_id
purpose = "ASYMMETRIC_SIGN"
labels = merge(
{
project = "burrow"
component = "identity"
managed_by = "opentofu"
},
var.tags,
try(each.value.labels, {}),
)
version_template {
algorithm = try(each.value.algorithm, "RSA_SIGN_PKCS1_2048_SHA256")
protection_level = try(each.value.protection_level, "HSM")
}
lifecycle {
prevent_destroy = true
}
}
resource "google_service_account" "runner" {
count = var.manage_google && var.manage_google_runner_service_account ? 1 : 0
project = var.google_project_id
account_id = var.google_runner_service_account_id
display_name = "Burrow Forgejo Runner"
description = "Authentik-backed WIF service account for Burrow Forgejo runners."
depends_on = [google_project_service.identity]
}
resource "google_iam_workload_identity_pool" "burrow" {
count = var.manage_google ? 1 : 0
project = var.google_project_id
workload_identity_pool_id = var.google_wif_pool_id
display_name = "Burrow Authentik"
description = "Authentik-issued identities for Burrow automation."
disabled = false
depends_on = [google_project_service.identity]
}
resource "google_iam_workload_identity_pool_provider" "forgejo_runners" {
count = var.manage_google ? 1 : 0
project = var.google_project_id
workload_identity_pool_id = google_iam_workload_identity_pool.burrow[0].workload_identity_pool_id
workload_identity_pool_provider_id = var.google_wif_provider_id
display_name = "Burrow Forgejo Runners"
description = "Authentik OIDC provider for Burrow Forgejo runner cloud access."
disabled = false
attribute_mapping = {
"google.subject" = "assertion.sub"
"google.groups" = "assertion.groups"
"attribute.burrow_subject" = "assertion.sub"
"attribute.burrow_username" = "assertion.preferred_username"
"attribute.burrow_email" = "assertion.email"
"attribute.burrow_groups" = "assertion.groups"
"attribute.burrow_client_id" = "assertion.azp"
}
attribute_condition = var.google_wif_attribute_condition
oidc {
issuer_uri = var.google_wif_issuer_uri
allowed_audiences = var.google_wif_allowed_audiences
}
}
resource "google_service_account_iam_member" "runner_wif" {
for_each = var.manage_google ? local.google_runner_wif_members : toset([])
service_account_id = var.manage_google_runner_service_account ? (
google_service_account.runner[0].name
) : "projects/${var.google_project_id}/serviceAccounts/${local.google_runner_service_account_email}"
role = "roles/iam.workloadIdentityUser"
member = each.value
}
resource "google_kms_crypto_key_iam_member" "signer" {
for_each = local.google_signer_grants
crypto_key_id = google_kms_crypto_key.signing[each.value.key_name].id
role = "roles/cloudkms.signerVerifier"
member = each.value.member
}

19
infra/identity/outputs.tf Normal file
View file

@ -0,0 +1,19 @@
output "google_kms_key_ring_id" {
description = "Google KMS key ring id used by the identity stack."
value = local.google_kms_key_ring_id
}
output "google_runner_service_account_email" {
description = "Forgejo runner service account email."
value = local.google_runner_service_account_email
}
output "google_wif_pool_name" {
description = "Google WIF pool resource name when managed."
value = try(google_iam_workload_identity_pool.burrow[0].name, null)
}
output "google_signing_key_ids" {
description = "Managed Google KMS signing key ids."
value = { for name, key in google_kms_crypto_key.signing : name => key.id }
}

View file

@ -0,0 +1,3 @@
provider "google" {
project = var.google_project_id
}

View file

@ -0,0 +1,15 @@
manage_google = false
google_project_id = "project-88c23ce9-918a-470a-b33"
google_project_number = "416198671487"
# Import the key ring before setting this true if it already exists.
manage_google_kms_key_ring = false
# Create this only after the Authentik WIF provider design is final.
manage_google_runner_service_account = false
google_wif_issuer_uri = "https://auth.burrow.net/application/o/google-cloud/"
google_wif_allowed_audiences = ["google-wif.burrow.net"]
google_wif_runner_group = "burrow-automation"
google_wif_runner_client_id = "google-wif.burrow.net"

201
infra/identity/variables.tf Normal file
View file

@ -0,0 +1,201 @@
variable "manage_google" {
description = "Manage Google Cloud identity and WIF resources."
type = bool
default = false
}
variable "google_project_id" {
description = "Google Cloud project id for Burrow identity resources."
type = string
default = "project-88c23ce9-918a-470a-b33"
}
variable "google_project_number" {
description = "Google Cloud project number for IAM principal URIs."
type = string
default = "416198671487"
}
variable "google_kms_key_ring_name" {
description = "Google KMS key ring for identity and release signing keys."
type = string
default = "burrow-identity"
}
variable "google_kms_key_ring_location" {
description = "Google KMS key ring location."
type = string
default = "global"
}
variable "google_kms_key_ring_id" {
description = "Existing Google KMS key ring id. Leave blank to derive it from project, location, and name."
type = string
default = ""
}
variable "manage_google_kms_key_ring" {
description = "Create/manage the Google KMS key ring. Import existing rings before enabling this."
type = bool
default = false
}
variable "google_signing_keys" {
description = "Non-exportable Burrow signing keys to create in the identity key ring."
type = map(object({
name = string
algorithm = optional(string, "RSA_SIGN_PKCS1_2048_SHA256")
protection_level = optional(string, "HSM")
labels = optional(map(string), {})
}))
default = {
authentik = {
name = "authentik-signing"
labels = {
key_target = "authentik"
}
}
saml_ca = {
name = "saml-ca"
labels = {
key_target = "saml-ca"
}
}
release = {
name = "release-signing"
labels = {
key_target = "release-signing"
}
}
package_apt_repository = {
name = "package-apt-repository"
labels = {
key_target = "package-apt-repository"
repo_format = "apt"
}
}
package_rpm_repository = {
name = "package-rpm-repository"
labels = {
key_target = "package-rpm-repository"
repo_format = "rpm"
}
}
package_pacman_repository = {
name = "package-pacman-repository"
labels = {
key_target = "package-pacman-repository"
repo_format = "pacman"
}
}
package_flatpak_repository = {
name = "package-flatpak-repository"
labels = {
key_target = "package-flatpak-repository"
repo_format = "flatpak"
}
}
package_aur_source = {
name = "package-aur-source"
labels = {
key_target = "package-aur-source"
repo_format = "aur"
}
}
apple_developer_id_application = {
name = "apple-developer-id-application"
labels = {
key_target = "apple-developer-id-application"
apple_cert_type = "developer-id-application"
}
}
apple_ios_distribution = {
name = "apple-ios-distribution"
labels = {
key_target = "apple-ios-distribution"
apple_cert_type = "ios-distribution"
}
}
sparkle_ed25519 = {
name = "sparkle-ed25519"
algorithm = "EC_SIGN_ED25519"
protection_level = "SOFTWARE"
labels = {
key_target = "sparkle-ed25519"
release_surface = "sparkle"
}
}
}
}
variable "google_release_signer_members" {
description = "Additional IAM members allowed to sign/verify with Burrow release and package repository keys."
type = set(string)
default = []
}
variable "manage_google_runner_service_account" {
description = "Create/manage the Forgejo runner service account for WIF."
type = bool
default = false
}
variable "google_runner_service_account_id" {
description = "Google service account id for Authentik-backed Forgejo runners."
type = string
default = "burrow-forgejo-runner"
}
variable "google_runner_service_account_email" {
description = "Existing runner service account email when not managed here."
type = string
default = "burrow-forgejo-runner@project-88c23ce9-918a-470a-b33.iam.gserviceaccount.com"
}
variable "google_wif_pool_id" {
description = "Workload Identity Federation pool id for Burrow automation."
type = string
default = "burrow-authentik"
}
variable "google_wif_provider_id" {
description = "Workload Identity Federation provider id for Authentik-issued OIDC tokens."
type = string
default = "forgejo-runners"
}
variable "google_wif_issuer_uri" {
description = "Authentik OIDC issuer used by Google WIF."
type = string
default = "https://auth.burrow.net/application/o/google-cloud/"
}
variable "google_wif_allowed_audiences" {
description = "Allowed audiences for Authentik-issued WIF tokens."
type = list(string)
default = ["google-wif.burrow.net"]
}
variable "google_wif_runner_group" {
description = "Mapped Authentik group allowed to assume the runner service account. Empty allows the whole pool."
type = string
default = "burrow-automation"
}
variable "google_wif_runner_client_id" {
description = "Mapped Authentik OIDC client ID allowed to assume the runner service account for machine-to-machine CI. Empty disables this grant."
type = string
default = "google-wif.burrow.net"
}
variable "google_wif_attribute_condition" {
description = "CEL condition applied to Authentik-issued WIF tokens."
type = string
default = "assertion.iss == 'https://auth.burrow.net/application/o/google-cloud/'"
}
variable "tags" {
description = "Additional labels applied to supported Google resources."
type = map(string)
default = {}
}

View file

@ -0,0 +1,12 @@
terraform {
required_version = ">= 1.8.0"
required_providers {
google = {
source = "hashicorp/google"
version = ">= 5.40.0, < 7.0"
}
}
backend "s3" {}
}

5
infra/openbao/.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
.terraform/
backend.hcl
terraform.tfstate
terraform.tfstate.backup
*.tfplan

43
infra/openbao/.terraform.lock.hcl generated Normal file
View file

@ -0,0 +1,43 @@
# This file is maintained automatically by "tofu init".
# Manual edits may be lost in future updates.
provider "registry.opentofu.org/hashicorp/google" {
version = "6.50.0"
constraints = ">= 5.40.0, < 7.0.0"
hashes = [
"h1:MAAe4zFFdqS9M5rpmJK/vKgdb6ZMD/s/0Xd97yTDipA=",
"zh:1d4695f807d998f11fcdcfa174766287b82a8093513af857bcdad2d81c642480",
"zh:3173ac5df0294624d113812e49e2a55714aff7db617488168cecdf4168df9e29",
"zh:34d2b3d44c23bd6354fc4ab5917b302872ea1ab8de107034567f955b1717fa5b",
"zh:3a77f3cc2f3664cd5aaeeef4d044e6ec1695a079588fffec3ca03953664e5f04",
"zh:6b444e4b629ea8dc8cb112a39dde098dc5584d26d6de4177558f556a9a226696",
"zh:96545c8cd4d3a57069c5d1799eab5aedd887e16d98b5559a195f6d2c2d9bc674",
"zh:ba464caafde95ee16671d6b5ec90f053ed77a9d06c567456db6efd9160fa3165",
"zh:d876938e5b0d3f57a984d9be72467995f87fef6569968623415dc51d9f54d30b",
"zh:dfd908d873e314ab807d0abc9cfd42d2611cd06dc1b9ec719ebdbb738e8e68d6",
"zh:f9f16819a7738d564afd45fd169ba61004ec4e4e7089d2a4950cb8895be1fe1f",
]
}
provider "registry.opentofu.org/hashicorp/vault" {
version = "5.9.0"
constraints = ">= 4.5.0, < 6.0.0"
hashes = [
"h1:9U4AT36IqNYXfQMR7nf+oywulVKYua473W4NmuwVb7M=",
"zh:03cb46fab606872e3fc06e27f8d14b911b7a9781d0f32af335c7287a9dbe4f4f",
"zh:1e69677b09329b009a8791f9cbb556790fb4dbddc1360f953345259892dc6c5b",
"zh:4e5a3dc82e70d88f5671968b7f2217163815be259d0b26dfcc9a897409c491fa",
"zh:51b82b358fe52099ef29cae62e2040ae0224ceea40a7dfdf0b69de90fb0233a8",
"zh:60ec3477fbe712b3269eec2e4d9d5921e2e68d0861d77e11e91bea088aab27bf",
"zh:64e58a3e3fd61005b54d71eee792b8576ab42ea4cd711515043a3ac0d8bc7919",
"zh:69142fb83aff31e27f5b3d9e7afb7594013b63e490cfcef8678fd0d724609359",
"zh:6f2e58ba6bd41c4d9e0e752e55eb7d86104f9d786cf57fa8dc960c61b574f9ce",
"zh:73390b7cb186310e5aaf4a1baaa639ab0302a8da4db130b4e28ebced3ba103fe",
"zh:a2214b3fda93cfed89ba1a7d714fe8b015176927605b215365978efdaae1c6db",
"zh:b275ee0f0a63765364d2e6979e3a11464d2c2ce6d3ce9bd15b3f503aea6f12b0",
"zh:bb2cd553b1c589a0f6f5b0575e1b176a9d91c1b20c6375d1afd63bb2a47fd014",
"zh:bc38e48ae5cdb8219fef326366f46e6a25e7f77f9bbcd9bdc80c7cb960de81ed",
"zh:e54af55cf667759a400af07b927b5034f1733e6527f9352b84001205561dcf9e",
"zh:f617688ef4568c0de8a28d5e1fa3714fb2eeab9f8cf7bcf20af42a1a511844be",
]
}

36
infra/openbao/README.md Normal file
View file

@ -0,0 +1,36 @@
# Burrow OpenBao Control Plane
This OpenTofu stack manages non-secret infrastructure around
`vault.burrow.net`.
It may manage:
- Google KMS seal-wrapping keys for OpenBao
- the OpenBao `age/` KV v2 mount used for migrated runtime secrets
- OpenBao policies and Authentik OIDC roles
- AppRole auth scaffolding for machine consumers
It must not manage:
- secret values
- root tokens
- AppRole SecretIDs
- release-signing key material
## Run
```sh
Scripts/openbao-tofu.sh init -backend-config=backend.hcl
Scripts/openbao-tofu.sh plan -var-file=terraform.tfvars
```
For local syntax work:
```sh
Scripts/openbao-tofu.sh init -backend=false
Scripts/openbao-tofu.sh validate
```
The Vault/OpenBao provider reads `VAULT_TOKEN` from the environment. Keep
`manage_openbao_config = false` until `vault.burrow.net` is initialized and an
operator token is available out of band.

View file

@ -0,0 +1,4 @@
bucket = "burrow-opentofu-state"
key = "openbao/openbao.tfstate"
region = "us-west-2"
encrypt = true

65
infra/openbao/google.tf Normal file
View file

@ -0,0 +1,65 @@
locals {
google_required_services = toset([
"cloudkms.googleapis.com",
"cloudresourcemanager.googleapis.com",
"iam.googleapis.com",
])
google_existing_kms_key_ring_id = var.google_kms_key_ring_id != "" ? var.google_kms_key_ring_id : "projects/${var.google_project_id}/locations/${var.google_kms_key_ring_location}/keyRings/${var.google_kms_key_ring_name}"
google_kms_key_ring_id = var.manage_google_kms_key_ring ? (
google_kms_key_ring.openbao[0].id
) : local.google_existing_kms_key_ring_id
}
resource "google_project_service" "openbao" {
for_each = var.manage_google ? local.google_required_services : toset([])
project = var.google_project_id
service = each.value
disable_on_destroy = false
}
resource "google_kms_key_ring" "openbao" {
count = var.manage_google && var.manage_google_kms_key_ring ? 1 : 0
project = var.google_project_id
location = var.google_kms_key_ring_location
name = var.google_kms_key_ring_name
depends_on = [google_project_service.openbao]
}
resource "google_kms_crypto_key" "openbao_seal" {
count = var.manage_google && local.google_kms_key_ring_id != "" ? 1 : 0
name = var.google_openbao_seal_key_name
key_ring = local.google_kms_key_ring_id
purpose = "ENCRYPT_DECRYPT"
labels = merge(
{
project = "burrow"
component = "openbao"
managed_by = "opentofu"
key_target = "seal"
},
var.tags,
)
version_template {
algorithm = "GOOGLE_SYMMETRIC_ENCRYPTION"
protection_level = "HSM"
}
lifecycle {
prevent_destroy = true
}
}
resource "google_kms_crypto_key_iam_member" "openbao_seal" {
for_each = var.manage_google && local.google_kms_key_ring_id != "" ? var.google_openbao_seal_members : toset([])
crypto_key_id = google_kms_crypto_key.openbao_seal[0].id
role = "roles/cloudkms.cryptoKeyEncrypterDecrypter"
member = each.value
}

125
infra/openbao/openbao.tf Normal file
View file

@ -0,0 +1,125 @@
resource "vault_mount" "age" {
count = var.manage_openbao_config ? 1 : 0
path = var.age_mount_path
type = "kv-v2"
description = "Migrated Burrow age-managed runtime secrets."
options = {
version = "2"
}
}
resource "vault_policy" "admin" {
count = var.manage_openbao_config ? 1 : 0
name = "burrow-openbao-admin"
policy = <<-EOT
path "*" {
capabilities = ["create", "read", "update", "delete", "list", "sudo"]
}
EOT
}
resource "vault_policy" "runtime_read" {
count = var.manage_openbao_config ? 1 : 0
name = "burrow-runtime-read"
policy = <<-EOT
path "${var.age_mount_path}/data/*" {
capabilities = ["read"]
}
path "${var.age_mount_path}/metadata/*" {
capabilities = ["list", "read"]
}
EOT
}
resource "vault_auth_backend" "approle" {
count = var.manage_openbao_config ? 1 : 0
path = var.approle_auth_path
type = "approle"
description = "Machine auth for Burrow runtime secret consumers."
tune {
token_type = "default-service"
}
}
resource "vault_approle_auth_backend_role" "runtime" {
count = var.manage_openbao_config ? 1 : 0
backend = vault_auth_backend.approle[0].path
role_name = var.runtime_approle_role_name
bind_secret_id = true
token_policies = [vault_policy.runtime_read[0].name]
token_ttl = var.runtime_approle_token_ttl_seconds
token_max_ttl = var.runtime_approle_token_max_ttl_seconds
token_num_uses = 0
secret_id_ttl = var.runtime_approle_secret_id_ttl_seconds
secret_id_num_uses = var.runtime_approle_secret_id_num_uses
}
resource "vault_jwt_auth_backend" "authentik" {
count = var.manage_openbao_config ? 1 : 0
path = "oidc"
type = "oidc"
oidc_discovery_url = var.authentik_issuer
bound_issuer = var.authentik_issuer
oidc_client_id = var.authentik_client_id
default_role = "burrow-admin"
tune {
listing_visibility = "unauth"
token_type = "default-service"
}
}
resource "vault_jwt_auth_backend_role" "admin" {
count = var.manage_openbao_config ? 1 : 0
backend = vault_jwt_auth_backend.authentik[0].path
role_name = "burrow-admin"
role_type = "oidc"
user_claim = "email"
allowed_redirect_uris = [
"https://vault.burrow.net/ui/vault/auth/oidc/oidc/callback",
"http://localhost:8250/oidc/callback",
]
bound_audiences = [var.authentik_client_id]
bound_claims = {
groups = var.authentik_admin_group
}
oidc_scopes = ["openid", "profile", "email", "groups"]
token_policies = [vault_policy.admin[0].name]
}
resource "vault_jwt_auth_backend_role" "runtime" {
count = var.manage_openbao_config ? 1 : 0
backend = vault_jwt_auth_backend.authentik[0].path
role_name = "burrow-runtime-read"
role_type = "oidc"
user_claim = "email"
allowed_redirect_uris = [
"https://vault.burrow.net/ui/vault/auth/oidc/oidc/callback",
"http://localhost:8250/oidc/callback",
]
bound_audiences = [var.authentik_client_id]
bound_claims = {
groups = var.authentik_runtime_group
}
oidc_scopes = ["openid", "profile", "email", "groups"]
token_policies = [vault_policy.runtime_read[0].name]
}

14
infra/openbao/outputs.tf Normal file
View file

@ -0,0 +1,14 @@
output "google_kms_key_ring_id" {
description = "Google KMS key ring id used by the OpenBao stack."
value = local.google_kms_key_ring_id
}
output "google_openbao_seal_key_id" {
description = "Google KMS seal key id when managed."
value = try(google_kms_crypto_key.openbao_seal[0].id, null)
}
output "openbao_age_mount_path" {
description = "OpenBao KV v2 mount path for migrated age secrets."
value = var.age_mount_path
}

View file

@ -0,0 +1,8 @@
provider "google" {
project = var.google_project_id
}
provider "vault" {
address = var.openbao_address
skip_tls_verify = var.openbao_skip_tls_verify
}

View file

@ -0,0 +1,8 @@
manage_google = false
manage_openbao_config = false
google_project_id = "project-88c23ce9-918a-470a-b33"
openbao_address = "https://vault.burrow.net"
authentik_issuer = "https://auth.burrow.net/application/o/openbao/"
authentik_client_id = "vault.burrow.net"
authentik_admin_group = "burrow-admins"
authentik_runtime_group = "burrow-automation"

137
infra/openbao/variables.tf Normal file
View file

@ -0,0 +1,137 @@
variable "openbao_address" {
description = "OpenBao API address. The Vault provider reads the token from VAULT_TOKEN."
type = string
default = "https://vault.burrow.net"
}
variable "openbao_skip_tls_verify" {
description = "Skip TLS verification for local bootstrap only."
type = bool
default = false
}
variable "age_mount_path" {
description = "KV v2 mount path for migrated age-managed runtime secrets."
type = string
default = "age"
}
variable "manage_openbao_config" {
description = "Manage OpenBao mounts, policies, and Authentik OIDC auth. Requires VAULT_TOKEN."
type = bool
default = false
}
variable "authentik_issuer" {
description = "Authentik OpenBao OIDC issuer."
type = string
default = "https://auth.burrow.net/application/o/openbao/"
}
variable "authentik_client_id" {
description = "Public Authentik OpenBao OIDC client id."
type = string
default = "vault.burrow.net"
}
variable "authentik_admin_group" {
description = "Authentik group allowed to administer OpenBao."
type = string
default = "burrow-admins"
}
variable "authentik_runtime_group" {
description = "Authentik group allowed to read runtime secrets."
type = string
default = "burrow-automation"
}
variable "approle_auth_path" {
description = "OpenBao AppRole auth mount path for machine consumers."
type = string
default = "approle"
}
variable "runtime_approle_role_name" {
description = "AppRole role name used by Burrow runtime secret consumers."
type = string
default = "burrow-runtime"
}
variable "runtime_approle_token_ttl_seconds" {
description = "Token TTL for runtime AppRole login."
type = number
default = 3600
}
variable "runtime_approle_token_max_ttl_seconds" {
description = "Maximum token TTL for runtime AppRole login."
type = number
default = 7200
}
variable "runtime_approle_secret_id_ttl_seconds" {
description = "SecretID TTL for runtime AppRole. Zero means no expiry; rotate manually."
type = number
default = 0
}
variable "runtime_approle_secret_id_num_uses" {
description = "SecretID use limit for runtime AppRole. Zero means unlimited uses; rotate manually."
type = number
default = 0
}
variable "manage_google" {
description = "Manage Google Cloud KMS resources for OpenBao seal wrapping."
type = bool
default = false
}
variable "google_project_id" {
description = "Google Cloud project id for OpenBao seal wrapping."
type = string
default = "project-88c23ce9-918a-470a-b33"
}
variable "manage_google_kms_key_ring" {
description = "Create/manage the Google KMS key ring. Existing rings should be imported before enabling this."
type = bool
default = false
}
variable "google_kms_key_ring_name" {
description = "Google KMS key ring for OpenBao seal wrapping."
type = string
default = "burrow-identity"
}
variable "google_kms_key_ring_location" {
description = "Google KMS key ring location."
type = string
default = "global"
}
variable "google_kms_key_ring_id" {
description = "Existing Google KMS key ring id."
type = string
default = ""
}
variable "google_openbao_seal_key_name" {
description = "Google KMS symmetric key used for OpenBao seal wrapping."
type = string
default = "openbao-seal"
}
variable "google_openbao_seal_members" {
description = "IAM members allowed to encrypt/decrypt with the OpenBao seal key."
type = set(string)
default = []
}
variable "tags" {
description = "Additional labels applied to supported Google resources."
type = map(string)
default = {}
}

16
infra/openbao/versions.tf Normal file
View file

@ -0,0 +1,16 @@
terraform {
required_version = ">= 1.8.0"
required_providers {
google = {
source = "hashicorp/google"
version = ">= 5.40.0, < 7.0"
}
vault = {
source = "hashicorp/vault"
version = ">= 4.5.0, < 6.0"
}
}
backend "s3" {}
}

5
infra/releases/.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
.terraform/
*.tfstate
*.tfstate.*
backend.hcl
terraform.tfvars

20
infra/releases/.terraform.lock.hcl generated Normal file
View file

@ -0,0 +1,20 @@
# This file is maintained automatically by "tofu init".
# Manual edits may be lost in future updates.
provider "registry.opentofu.org/hashicorp/google" {
version = "6.50.0"
constraints = ">= 5.40.0, < 7.0.0"
hashes = [
"h1:MAAe4zFFdqS9M5rpmJK/vKgdb6ZMD/s/0Xd97yTDipA=",
"zh:1d4695f807d998f11fcdcfa174766287b82a8093513af857bcdad2d81c642480",
"zh:3173ac5df0294624d113812e49e2a55714aff7db617488168cecdf4168df9e29",
"zh:34d2b3d44c23bd6354fc4ab5917b302872ea1ab8de107034567f955b1717fa5b",
"zh:3a77f3cc2f3664cd5aaeeef4d044e6ec1695a079588fffec3ca03953664e5f04",
"zh:6b444e4b629ea8dc8cb112a39dde098dc5584d26d6de4177558f556a9a226696",
"zh:96545c8cd4d3a57069c5d1799eab5aedd887e16d98b5559a195f6d2c2d9bc674",
"zh:ba464caafde95ee16671d6b5ec90f053ed77a9d06c567456db6efd9160fa3165",
"zh:d876938e5b0d3f57a984d9be72467995f87fef6569968623415dc51d9f54d30b",
"zh:dfd908d873e314ab807d0abc9cfd42d2611cd06dc1b9ec719ebdbb738e8e68d6",
"zh:f9f16819a7738d564afd45fd169ba61004ec4e4e7089d2a4950cb8895be1fe1f",
]
}

54
infra/releases/README.md Normal file
View file

@ -0,0 +1,54 @@
# Burrow Release Storage
This OpenTofu stack owns Google Cloud Storage buckets used as Burrow's first
release-storage backup target.
Garage is the required S3-compatible primary target. CI uses repo-owned storage
wrappers: they upload to Garage first, then mirror to GCS unless the backup is
explicitly disabled.
The host activation point is `services.burrow.garage`, enabled by the forge
host. It creates the initial single-node Garage layout and buckets from
host-local runtime secrets.
It may manage:
- the release artifact bucket served by `releases.burrow.net`
- the signed package repository bucket served by `packages.burrow.net`
- the private Nix cache backup bucket for `nix.burrow.net`
- public object-read IAM for download buckets
- object-admin IAM for the Burrow Forgejo runner service account
It must not manage:
- Google service-account JSON keys
- release signing keys
- package repository metadata
- DNS or CDN cutover
GCS access is keyless. Forgejo jobs run `Scripts/ci/google-wif-auth.sh` to
trade an Authentik-issued OIDC token for the Google runner service account in
project `project-88c23ce9-918a-470a-b33`, then use `gcloud storage` against the
managed backup buckets. The normal release path does not use rclone or Google
service-account JSON keys.
The Nix cache backup bucket defaults to private. Attic remains the public cache
surface at `nix.burrow.net`; GCS is only a backup target for the Garage `attic`
bucket.
## Run
```sh
Scripts/releases-tofu.sh init -backend-config=backend.hcl
Scripts/releases-tofu.sh plan -var-file=terraform.tfvars
```
For local syntax work without remote state:
```sh
Scripts/releases-tofu.sh init -backend=false
Scripts/releases-tofu.sh validate
```
Start with `manage_google = false`. Import existing buckets before enabling
management if they already exist.

View file

@ -0,0 +1,3 @@
bucket = "burrow-opentofu-state"
key = "releases/releases.tfstate"
region = "us-west-2"

29
infra/releases/outputs.tf Normal file
View file

@ -0,0 +1,29 @@
output "google_release_bucket_name" {
description = "GCS bucket used for release artifacts."
value = try(google_storage_bucket.release_storage["releases"].name, var.google_release_bucket_name)
}
output "google_package_bucket_name" {
description = "GCS bucket used for signed package repositories."
value = try(google_storage_bucket.release_storage["packages"].name, var.google_package_bucket_name)
}
output "google_nix_cache_bucket_name" {
description = "GCS bucket used for Nix cache backups."
value = try(google_storage_bucket.release_storage["nix-cache"].name, var.google_nix_cache_bucket_name)
}
output "google_release_bucket_uri" {
description = "GCS URI for release artifacts."
value = "gs://${try(google_storage_bucket.release_storage["releases"].name, var.google_release_bucket_name)}"
}
output "google_package_bucket_uri" {
description = "GCS URI for signed package repositories."
value = "gs://${try(google_storage_bucket.release_storage["packages"].name, var.google_package_bucket_name)}"
}
output "google_nix_cache_bucket_uri" {
description = "GCS URI for Nix cache backups."
value = "gs://${try(google_storage_bucket.release_storage["nix-cache"].name, var.google_nix_cache_bucket_name)}"
}

View file

@ -0,0 +1,3 @@
provider "google" {
project = var.google_project_id
}

116
infra/releases/storage.tf Normal file
View file

@ -0,0 +1,116 @@
locals {
google_required_services = toset([
"storage.googleapis.com",
])
release_storage_buckets = {
releases = {
name = var.google_release_bucket_name
description = "Burrow release artifacts"
public = var.google_release_public_read
}
packages = {
name = var.google_package_bucket_name
description = "Burrow signed package repositories"
public = var.google_package_public_read
}
nix-cache = {
name = var.google_nix_cache_bucket_name
description = "Burrow Nix cache backups"
public = var.google_nix_cache_public_read
}
}
release_storage_writer_members = setunion(
var.google_release_extra_writer_members,
toset(["serviceAccount:${var.google_runner_service_account_email}"]),
)
release_storage_writer_grants = var.manage_google ? {
for grant in flatten([
for bucket_key in keys(local.release_storage_buckets) : [
for member in local.release_storage_writer_members : {
id = "${bucket_key}|${member}"
bucket_key = bucket_key
member = member
}
]
]) : grant.id => grant
} : {}
release_storage_public_grants = var.manage_google ? {
for bucket_key, bucket in local.release_storage_buckets : bucket_key => bucket
if bucket.public
} : {}
}
resource "google_project_service" "release_storage" {
for_each = var.manage_google ? local.google_required_services : toset([])
project = var.google_project_id
service = each.value
disable_on_destroy = false
}
resource "google_storage_bucket" "release_storage" {
for_each = var.manage_google ? local.release_storage_buckets : {}
project = var.google_project_id
name = each.value.name
location = var.google_storage_location
uniform_bucket_level_access = true
public_access_prevention = each.value.public ? "inherited" : "enforced"
force_destroy = false
labels = merge(
{
project = "burrow"
component = "release-storage"
managed_by = "opentofu"
bucket = each.key
},
var.tags,
)
versioning {
enabled = true
}
cors {
origin = var.google_storage_cors_origins
method = ["GET", "HEAD", "OPTIONS"]
response_header = ["Content-Type", "Content-Length", "ETag", "Cache-Control"]
max_age_seconds = 3600
}
lifecycle_rule {
condition {
age = 30
with_state = "ARCHIVED"
num_newer_versions = 5
matches_storage_class = ["STANDARD"]
}
action {
type = "Delete"
}
}
depends_on = [google_project_service.release_storage]
}
resource "google_storage_bucket_iam_member" "release_storage_public_viewer" {
for_each = local.release_storage_public_grants
bucket = google_storage_bucket.release_storage[each.key].name
role = "roles/storage.objectViewer"
member = "allUsers"
}
resource "google_storage_bucket_iam_member" "release_storage_writer" {
for_each = local.release_storage_writer_grants
bucket = google_storage_bucket.release_storage[each.value.bucket_key].name
role = "roles/storage.objectAdmin"
member = each.value.member
}

View file

@ -0,0 +1,12 @@
manage_google = false
google_project_id = "project-88c23ce9-918a-470a-b33"
google_storage_location = "US"
google_runner_service_account_email = "burrow-forgejo-runner@project-88c23ce9-918a-470a-b33.iam.gserviceaccount.com"
google_release_bucket_name = "burrow-net-releases"
google_package_bucket_name = "burrow-net-packages"
google_nix_cache_bucket_name = "burrow-net-nix-cache"
google_release_public_read = true
google_package_public_read = true
google_nix_cache_public_read = false
google_release_extra_writer_members = []

View file

@ -0,0 +1,81 @@
variable "manage_google" {
description = "Manage Google Cloud release storage resources."
type = bool
default = false
}
variable "google_project_id" {
description = "Google Cloud project id for Burrow release storage."
type = string
default = "project-88c23ce9-918a-470a-b33"
}
variable "google_storage_location" {
description = "Google Cloud Storage bucket location for release, package, and cache backup buckets."
type = string
default = "US"
}
variable "google_runner_service_account_email" {
description = "Forgejo runner service account email allowed to publish release, package, and cache backup objects."
type = string
default = "burrow-forgejo-runner@project-88c23ce9-918a-470a-b33.iam.gserviceaccount.com"
}
variable "google_release_bucket_name" {
description = "GCS bucket for release artifacts served by releases.burrow.net."
type = string
default = "burrow-net-releases"
}
variable "google_package_bucket_name" {
description = "GCS bucket for signed package repositories served by packages.burrow.net."
type = string
default = "burrow-net-packages"
}
variable "google_nix_cache_bucket_name" {
description = "GCS backup bucket for the Garage-backed Burrow Nix cache."
type = string
default = "burrow-net-nix-cache"
}
variable "google_release_public_read" {
description = "Grant allUsers objectViewer on the release artifact bucket."
type = bool
default = true
}
variable "google_package_public_read" {
description = "Grant allUsers objectViewer on the package repository bucket."
type = bool
default = true
}
variable "google_nix_cache_public_read" {
description = "Grant allUsers objectViewer on the Nix cache backup bucket."
type = bool
default = false
}
variable "google_release_extra_writer_members" {
description = "Additional IAM members allowed to write release, repository, and cache backup objects."
type = set(string)
default = []
}
variable "google_storage_cors_origins" {
description = "Allowed origins for public release/package downloads."
type = list(string)
default = [
"https://burrow.net",
"https://releases.burrow.net",
"https://packages.burrow.net",
]
}
variable "tags" {
description = "Additional labels applied to supported Google resources."
type = map(string)
default = {}
}

View file

@ -0,0 +1,12 @@
terraform {
required_version = ">= 1.8.0"
required_providers {
google = {
source = "hashicorp/google"
version = ">= 5.40.0, < 7.0"
}
}
backend "s3" {}
}