diff --git a/.gitea/workflows/iac.yaml b/.gitea/workflows/iac.yaml index 7fa06b2..3b449eb 100644 --- a/.gitea/workflows/iac.yaml +++ b/.gitea/workflows/iac.yaml @@ -29,8 +29,8 @@ concurrency: path: gitea_jwt secrets: | kvv1/google/credentials credentials | GOOGLE_CREDENTIALS ; - kvv1/admin/gitea token | GITEA_TOKEN - + kvv1/admin/gitea token | GITEA_TOKEN ; + kvv1/admin/cloudflare iam_token | CLOUDFLARE_API_TOKEN ; jobs: gitea_vault_auth: name: Auth with gitea for vault diff --git a/iac/cloudflare_ovh_cms.tf b/iac/cloudflare_ovh_cms.tf new file mode 100644 index 0000000..3f68801 --- /dev/null +++ b/iac/cloudflare_ovh_cms.tf @@ -0,0 +1,76 @@ +data "cloudflare_account" "arcodange" { + filter = { + name = "arcodange@gmail.com" + } +} + +locals { + cloudflare_account_id = data.cloudflare_account.arcodange.account_id +} + +resource "cloudflare_r2_bucket" "arcodange_tf" { + account_id = local.cloudflare_account_id + name = "arcodange-tf" + jurisdiction = "eu" +} + +module "cf_r2_arcodange_tf_token" { + source = "./modules/cloudflare_token" + account_id = local.cloudflare_account_id + bucket = cloudflare_r2_bucket.arcodange_tf + token_name = "r2_arcodange_tf_token" + permissions = { + bucket = [ + "account:Workers R2 Storage Read", + "bucket:Workers R2 Storage Bucket Item Write", + ] + } +} +resource "vault_kv_secret" "cf_r2_arcodange_tf" { + path = "kvv1/cloudflare/r2/arcodange-tf" + data_json = jsonencode({ + S3_SECRET_ACCESS_KEY = module.cf_r2_arcodange_tf_token.r2_credentials.secret_access_key + S3_ACCESS_KEY = module.cf_r2_arcodange_tf_token.r2_credentials.access_key_id + }) +} + +data "vault_policy_document" "cf_r2_arcodange_tf" { + rule { + path = "kvv1/cloudflare/r2/arcodange-tf" + capabilities = ["read"] + } +} +resource "vault_policy" "cf_r2_arcodange_tf" { + name = "factory__cf_r2_arcodange_tf" + policy = data.vault_policy_document.cf_r2_arcodange_tf.hcl +} + +data "gitea_repo" "cms" { + name = "cms" + username = "arcodange-org" +} +module "cf_arcodange_cms_token" { + source = "./modules/cloudflare_token" + account_id = local.cloudflare_account_id + bucket = cloudflare_r2_bucket.arcodange_tf + token_name = "cf_arcodange_cms_token" + permissions = { + account = [ + "account:Pages Write", + "account:Account DNS Settings Write", + ] + } +} +resource "gitea_repository_actions_secret" "cf_arcodange_cms_token" { + repository = data.gitea_repo.cms.name + repository_owner = data.gitea_repo.cms.username + secret_name = "CLOUDFLARE_API_TOKEN" + secret_value = module.cf_arcodange_cms_token.token +} + +resource "vault_kv_secret" "cf_arcodange_cms_token" { + path = "kvv1/cloudflare/cms/cf_arcodange_cms_token" + data_json = jsonencode({ + token = module.cf_arcodange_cms_token.token + }) +} \ No newline at end of file diff --git a/iac/gcs_backup.tf b/iac/gcs_backup.tf index 73b5c29..c2a2f48 100644 --- a/iac/gcs_backup.tf +++ b/iac/gcs_backup.tf @@ -35,9 +35,9 @@ resource "vault_kv_secret_v2" "longhorn_gcs_backup" { cas = 1 delete_all_versions = true data_json = jsonencode({ - AWS_ACCESS_KEY_ID = google_storage_hmac_key.longhorn_backup.access_id + AWS_ACCESS_KEY_ID = google_storage_hmac_key.longhorn_backup.access_id AWS_SECRET_ACCESS_KEY = google_storage_hmac_key.longhorn_backup.secret - AWS_ENDPOINTS: "https://storage.googleapis.com" + AWS_ENDPOINTS : "https://storage.googleapis.com" }) } diff --git a/iac/modules/cloudflare_token/main.tf b/iac/modules/cloudflare_token/main.tf new file mode 100644 index 0000000..3fb695c --- /dev/null +++ b/iac/modules/cloudflare_token/main.tf @@ -0,0 +1,79 @@ +# Récupère toutes les permissions Cloudflare disponibles +data "cloudflare_account_api_token_permission_groups_list" "all" { + account_id = var.account_id +} + +# Sélectionne uniquement les permissions demandées +locals { + # Simplifie le scope Cloudflare (ex: "account" depuis "com.cloudflare.api.account") + permission_map = { + for p in data.cloudflare_account_api_token_permission_groups_list.all.result : + "${split(".", p.scopes[0])[length(split(".", p.scopes[0])) - 1]}:${p.name}" => p.id + } + + # Résout les permissions (si présentes) pour chaque catégorie + selected_account_permissions = var.permissions.account != null ? compact([ + for name in var.permissions.account : lookup(local.permission_map, name, null) + ]) : [] + + selected_bucket_permissions = var.bucket != null && try(var.permissions.bucket, null) != null ? compact([ + for name in var.permissions.bucket : lookup(local.permission_map, name, null) + ]) : [] + + # Validation des permissions introuvables + missing_permissions = concat( + [for name in coalesce(var.permissions.account, []) : name if lookup(local.permission_map, name, null) == null], + [for name in coalesce(var.permissions.bucket, []) : name if lookup(local.permission_map, name, null) == null] + ) + + # Ressources cibles + account_resource = { + "com.cloudflare.api.account.${var.account_id}" = "*" + } + + bucket_resource = var.bucket != null ? { + "com.cloudflare.edge.r2.bucket.${var.account_id}_${var.bucket.jurisdiction}_${var.bucket.name}" = "*" + } : {} + + # Policies construites dynamiquement + policies = [for policy in [ + length(local.selected_account_permissions) > 0 ? { + effect = "allow" + permission_groups = [for id in local.selected_account_permissions : { id = id }] + resources = local.account_resource + } : null, + + length(local.selected_bucket_permissions) > 0 ? { + effect = "allow" + permission_groups = [for id in local.selected_bucket_permissions : { id = id }] + resources = local.bucket_resource + } : null + ] : policy if policy != null] + + error_message = length(local.missing_permissions) > 0 ? format("Permissions introuvables : %s", join(", ", local.missing_permissions)) : "" +} + +# Création du token +resource "cloudflare_account_token" "token" { + account_id = var.account_id + name = var.token_name + + policies = local.policies + + expires_on = null + + lifecycle { + ignore_changes = [expires_on] + replace_triggered_by = [null_resource.cloudflare_account_token_replace] + precondition { + condition = length(local.missing_permissions) == 0 + error_message = local.error_message + } + } +} + +resource "null_resource" "cloudflare_account_token_replace" { + triggers = { + "policies" = sha256(join("", local.selected_account_permissions, local.selected_bucket_permissions)) + } +} diff --git a/iac/modules/cloudflare_token/outputs.tf b/iac/modules/cloudflare_token/outputs.tf new file mode 100644 index 0000000..e116ea7 --- /dev/null +++ b/iac/modules/cloudflare_token/outputs.tf @@ -0,0 +1,35 @@ +output "token" { + description = "Valeur du token Cloudflare" + value = cloudflare_account_token.token.value + sensitive = true +} + +output "token_id" { + description = "ID du token Cloudflare (sert de Access Key ID pour R2 si bucket défini)" + value = cloudflare_account_token.token.id +} + +output "token_sha256" { + description = "SHA-256 du token Cloudflare (sert de Secret Access Key pour R2 si bucket défini)" + value = sha256(cloudflare_account_token.token.value) + sensitive = true +} + +output "r2_credentials" { + description = "Credentials R2 si bucket configuré (AccessKeyId, SecretAccessKey)" + value = var.bucket != null ? { + access_key_id = cloudflare_account_token.token.id + secret_access_key = sha256(cloudflare_account_token.token.value) + } : null + sensitive = true +} + +output "permissions" { + description = "Liste des permissions introuvables (si existantes)" + value = compact(concat(local.selected_account_permissions, local.selected_bucket_permissions)) +} + +output "resources" { + description = "Map des resources assignées au token" + value = keys(merge(local.account_resource, local.bucket_resource)) +} diff --git a/iac/modules/cloudflare_token/variables.tf b/iac/modules/cloudflare_token/variables.tf new file mode 100644 index 0000000..c159248 --- /dev/null +++ b/iac/modules/cloudflare_token/variables.tf @@ -0,0 +1,37 @@ +variable "account_id" { + description = "Cloudflare account ID" + type = string +} + +variable "token_name" { + description = "Nom du token Cloudflare à créer" + type = string +} + +variable "permissions" { + description = <<-EOT + Liste des permissions Cloudflare (ex: [\"Pages Deploy\", \"Zone DNS Edit\"]) + you can check required permissions per service + https://developers.cloudflare.com/api/node/ + EOT + type = object({ + account = optional(list(string)) + bucket = optional(list(string)) + }) +} + +variable "bucket" { + description = <<-EOT + Objet optionnel représentant un bucket R2. + Exemple : + { + name = "mon-bucket" + jurisdiction = "eu" + } + EOT + type = object({ + name = string + jurisdiction = string + }) + default = null +} diff --git a/iac/providers.tf b/iac/providers.tf index 8c829e0..e5c811d 100644 --- a/iac/providers.tf +++ b/iac/providers.tf @@ -2,7 +2,7 @@ terraform { required_providers { gitea = { source = "go-gitea/gitea" - version = "0.5.1" + version = "0.6.0" } vault = { source = "vault" @@ -12,6 +12,10 @@ terraform { source = "google" version = "7.0.1" } + cloudflare = { + source = "cloudflare/cloudflare" + version = "~> 5" + } } } @@ -31,4 +35,6 @@ provider "vault" { provider "google" { project = "arcodange" region = "US-EAST1" -} \ No newline at end of file +} + +provider "cloudflare" {} # CLOUDFLARE_API_TOKEN environment variable required \ No newline at end of file