diff --git a/.github/workflows/documentation.yaml b/.github/workflows/documentation.yaml index e8743405..584462af 100644 --- a/.github/workflows/documentation.yaml +++ b/.github/workflows/documentation.yaml @@ -11,6 +11,7 @@ jobs: fail-fast: false matrix: module: + - audit-serviceaccount - authorize-private-service - bucket-events - cloudevent-broker diff --git a/modules/audit-serviceaccount/README.md b/modules/audit-serviceaccount/README.md new file mode 100644 index 00000000..e8f9dfa4 --- /dev/null +++ b/modules/audit-serviceaccount/README.md @@ -0,0 +1,73 @@ +# `audit-serviceaccount` + +This module creates an alert policy to monitor the principals that are +generating tokens for a particular service account. + +The set of authorized principals can be enumerated explicitly: +```hcl +module "audit-foo-usage" { + source = "chainguard-dev/common/infra//modules/audit-serviceaccount" + + project_id = var.project_id + service-account = google_service_account.foo.email + + allowed_principals = [ + # Only GKE should generate tokens for this service account. + "serviceAccount:${var.project_id}.svc.id.goog[foo-system/foo]", + ] + + notification_channels = var.notification_channels +} +``` + +Or a regular expression can be provided for the allowed principals: +```hcl +module "audit-foo-usage" { + source = "chainguard-dev/common/infra//modules/audit-serviceaccount" + + project_id = var.project_id + service-account = google_service_account.foo.email + + # Match v1.2.3 style tags on this repository. + allowed_principal_regex = "principal://iam[.]googleapis[.]com/${google_iam_workload_identity_pool.pool.name}/subject/repo:chainguard-dev/terraform-infra-common:ref:refs/tags/v[0-9]+[.][0-9]+[.][0-9]+" + + notification_channels = var.notification_channels +} +``` + + + +## Requirements + +No requirements. + +## Providers + +| Name | Version | +|------|---------| +| [google](#provider\_google) | n/a | + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| [google_monitoring_alert_policy.generate-access-token](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/monitoring_alert_policy) | resource | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [allowed\_principal\_regex](#input\_allowed\_principal\_regex) | A regular expression to match allowed principals. | `string` | `""` | no | +| [allowed\_principals](#input\_allowed\_principals) | The list of principals authorized to assume this identity. | `list(string)` | `[]` | no | +| [notification\_channels](#input\_notification\_channels) | The list of notification channels to alert when this policy fires. | `list(string)` | n/a | yes | +| [project\_id](#input\_project\_id) | n/a | `string` | n/a | yes | +| [service-account](#input\_service-account) | The email of the service account being audited. | `string` | n/a | yes | + +## Outputs + +No outputs. + diff --git a/modules/audit-serviceaccount/main.tf b/modules/audit-serviceaccount/main.tf new file mode 100644 index 00000000..cbb75ffd --- /dev/null +++ b/modules/audit-serviceaccount/main.tf @@ -0,0 +1,43 @@ +resource "google_monitoring_alert_policy" "generate-access-token" { + count = length(var.notification_channels) > 0 ? 1 : 0 + + # In the absence of data, incident will auto-close after an hour + alert_strategy { + auto_close = "3600s" + + notification_rate_limit { + period = "3600s" // re-alert hourly if condition still valid. + } + } + + display_name = "Abnormal Access Token Generation: ${var.service-account}" + combiner = "OR" + + conditions { + display_name = "Access Token Generation" + + condition_matched_log { + filter = < [audit-delivery-serviceaccount](#module\_audit-delivery-serviceaccount) | ../audit-serviceaccount | n/a | | [authorize-delivery](#module\_authorize-delivery) | ../authorize-private-service | n/a | | [http](#module\_http) | ../dashboard/sections/http | n/a | | [layout](#module\_layout) | ../dashboard/sections/layout | n/a | diff --git a/modules/bucket-events/main.tf b/modules/bucket-events/main.tf index f8f621e5..77ae5e9c 100644 --- a/modules/bucket-events/main.tf +++ b/modules/bucket-events/main.tf @@ -55,6 +55,22 @@ resource "google_service_account_iam_binding" "allow-pubsub-to-mint-tokens" { members = ["serviceAccount:${google_project_service_identity.pubsub.email}"] } +module "audit-delivery-serviceaccount" { + count = length(var.notification_channels) > 0 ? 1 : 0 + + source = "../audit-serviceaccount" + + project_id = var.project_id + service-account = google_service_account.delivery.email + + # The absence of authorized identities here means that + # nothing is authorized to act as this service account. + # Note: Cloud Pub/Sub's usage doesn't show up in the + # audit logs. + + notification_channels = var.notification_channels +} + module "this" { source = "../regional-go-service" project_id = var.project_id diff --git a/modules/cloudevent-recorder/README.md b/modules/cloudevent-recorder/README.md index 02f94a39..ed76be57 100644 --- a/modules/cloudevent-recorder/README.md +++ b/modules/cloudevent-recorder/README.md @@ -93,6 +93,7 @@ No requirements. | Name | Source | Version | |------|--------|---------| +| [audit-import-serviceaccount](#module\_audit-import-serviceaccount) | ../audit-serviceaccount | n/a | | [recorder-dashboard](#module\_recorder-dashboard) | ../dashboard/cloudevent-receiver | n/a | | [this](#module\_this) | ../regional-go-service | n/a | | [triggers](#module\_triggers) | ../cloudevent-trigger | n/a | @@ -107,6 +108,7 @@ No requirements. | [google_bigquery_table.types](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/bigquery_table) | resource | | [google_bigquery_table_iam_binding.import-writes-to-tables](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/bigquery_table_iam_binding) | resource | | [google_monitoring_alert_policy.bq_dts](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/monitoring_alert_policy) | resource | +| [google_monitoring_alert_policy.bucket-access](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/monitoring_alert_policy) | resource | | [google_pubsub_subscription.dead-letter-pull-sub](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/pubsub_subscription) | resource | | [google_pubsub_subscription.this](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/pubsub_subscription) | resource | | [google_pubsub_subscription_iam_binding.allow-pubsub-to-ack](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/pubsub_subscription_iam_binding) | resource | diff --git a/modules/cloudevent-recorder/bigquery.tf b/modules/cloudevent-recorder/bigquery.tf index 64a3b212..c2b85b7c 100644 --- a/modules/cloudevent-recorder/bigquery.tf +++ b/modules/cloudevent-recorder/bigquery.tf @@ -74,6 +74,22 @@ resource "google_service_account_iam_binding" "provisioner-acts-as-import-identi members = [var.provisioner] } +module "audit-import-serviceaccount" { + count = length(var.notification_channels) > 0 ? 1 : 0 + + source = "../audit-serviceaccount" + + project_id = var.project_id + service-account = google_service_account.import-identity.email + + # The absence of authorized identities here means that + # nothing is authorized to act as this service account. + # Note: BigQuery DTS's usage doesn't show up in the + # audit logs. + + notification_channels = var.notification_channels +} + // Create a BQ DTS job for each of the regions x types pulling from the appropriate buckets and paths. resource "google_bigquery_data_transfer_config" "import-job" { for_each = local.regional-types diff --git a/modules/cloudevent-recorder/main.tf b/modules/cloudevent-recorder/main.tf index 4dbf9938..645d0129 100644 --- a/modules/cloudevent-recorder/main.tf +++ b/modules/cloudevent-recorder/main.tf @@ -50,3 +50,70 @@ resource "google_storage_bucket" "recorder" { // What identity is deploying this? data "google_client_openid_userinfo" "me" {} +resource "google_monitoring_alert_policy" "bucket-access" { + count = length(var.notification_channels) > 0 ? 1 : 0 + + # In the absence of data, incident will auto-close after an hour + alert_strategy { + auto_close = "3600s" + + notification_rate_limit { + period = "3600s" // re-alert hourly if condition still valid. + } + } + + display_name = "Abnormal Event Bucket Access: ${var.name}" + combiner = "OR" + + conditions { + display_name = "Bucket Access" + + condition_matched_log { + filter = < [audit-trigger-serviceaccount](#module\_audit-trigger-serviceaccount) | ../audit-serviceaccount | n/a | | [authorize-delivery](#module\_authorize-delivery) | ../authorize-private-service | n/a | ## Resources diff --git a/modules/cloudevent-trigger/main.tf b/modules/cloudevent-trigger/main.tf index c0dd607b..a79115c7 100644 --- a/modules/cloudevent-trigger/main.tf +++ b/modules/cloudevent-trigger/main.tf @@ -30,6 +30,22 @@ resource "google_service_account_iam_binding" "allow-pubsub-to-mint-tokens" { members = ["serviceAccount:${google_project_service_identity.pubsub.email}"] } +module "audit-trigger-serviceaccount" { + count = length(var.notification_channels) > 0 ? 1 : 0 + + source = "../audit-serviceaccount" + + project_id = var.project_id + service-account = google_service_account.this.email + + # The absence of authorized identities here means that + # nothing is authorized to act as this service account. + # Note: Cloud Pub/Sub's usage doesn't show up in the + # audit logs. + + notification_channels = var.notification_channels +} + // Authorize this service account to invoke the private service receiving // events from this trigger. module "authorize-delivery" { diff --git a/modules/configmap/README.md b/modules/configmap/README.md index 994bf245..2c845283 100644 --- a/modules/configmap/README.md +++ b/modules/configmap/README.md @@ -22,7 +22,7 @@ module "my-configmap" { EOT # Optionally: channels to notify if this configuration is manipulated. - notification-channels = [ ... ] + notification_channels = [ ... ] } module "foo-service" { @@ -77,6 +77,7 @@ No modules. | Name | Type | |------|------| +| [google_monitoring_alert_policy.anomalous-secret-access](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/monitoring_alert_policy) | resource | | [google_secret_manager_secret.this](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/secret_manager_secret) | resource | | [google_secret_manager_secret_iam_binding.authorize-access](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/secret_manager_secret_iam_binding) | resource | | [google_secret_manager_secret_version.data](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/secret_manager_secret_version) | resource | @@ -89,7 +90,7 @@ No modules. |------|-------------|------|---------|:--------:| | [data](#input\_data) | The data to place in the secret. | `string` | n/a | yes | | [name](#input\_name) | The name to give the secret. | `string` | n/a | yes | -| [notification-channels](#input\_notification-channels) | The channels to notify if the configuration data is improperly accessed. | `list(string)` | n/a | yes | +| [notification\_channels](#input\_notification\_channels) | The channels to notify if the configuration data is improperly accessed. | `list(string)` | n/a | yes | | [project\_id](#input\_project\_id) | n/a | `string` | n/a | yes | | [service-account](#input\_service-account) | The email of the service account that will access the secret. | `string` | n/a | yes | diff --git a/modules/configmap/main.tf b/modules/configmap/main.tf index a91e401c..79481eee 100644 --- a/modules/configmap/main.tf +++ b/modules/configmap/main.tf @@ -28,3 +28,57 @@ data "google_project" "project" { project_id = var.project_id } // What identity is deploying this? data "google_client_openid_userinfo" "me" {} +// Create an alert policy to notify if the secret is accessed by an unauthorized entity. +resource "google_monitoring_alert_policy" "anomalous-secret-access" { + count = length(var.notification_channels) > 0 ? 1 : 0 + + # In the absence of data, incident will auto-close after an hour + alert_strategy { + auto_close = "3600s" + + notification_rate_limit { + period = "3600s" // re-alert hourly if condition still valid. + } + } + + display_name = "Abnormal ConfigMap Access: ${var.name}" + combiner = "OR" + + conditions { + display_name = "Abnormal ConfigMap Access: ${var.name}" + + condition_matched_log { + filter = < [audit-cronjob-serviceaccount](#module\_audit-cronjob-serviceaccount) | ../audit-serviceaccount | n/a | +| [audit-delivery-serviceaccount](#module\_audit-delivery-serviceaccount) | ../audit-serviceaccount | n/a | ## Resources @@ -79,6 +82,8 @@ No modules. | [google-beta_google_cloud_run_v2_job.job](https://registry.terraform.io/providers/hashicorp/google-beta/latest/docs/resources/google_cloud_run_v2_job) | resource | | [google_cloud_run_v2_job_iam_binding.authorize-calls](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/cloud_run_v2_job_iam_binding) | resource | | [google_cloud_scheduler_job.cron](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/cloud_scheduler_job) | resource | +| [google_monitoring_alert_policy.anomalous-job-access](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/monitoring_alert_policy) | resource | +| [google_monitoring_alert_policy.anomalous-job-execution](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/monitoring_alert_policy) | resource | | [google_monitoring_alert_policy.success](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/monitoring_alert_policy) | resource | | [google_project_iam_member.authorize-list](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/project_iam_member) | resource | | [google_project_iam_member.metrics-writer](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/project_iam_member) | resource | diff --git a/modules/cron/main.tf b/modules/cron/main.tf index 876b735a..3e70d16a 100644 --- a/modules/cron/main.tf +++ b/modules/cron/main.tf @@ -18,6 +18,20 @@ resource "google_project_service" "cloudscheduler" { disable_on_destroy = false } +module "audit-cronjob-serviceaccount" { + source = "../audit-serviceaccount" + + project_id = var.project_id + service-account = var.service_account + + # The absence of authorized identities here means that + # nothing is authorized to act as this service account. + # Note: Cloud Run's usage doesn't show up in the + # audit logs. + + notification_channels = var.notification_channels +} + locals { repo = var.repository != "" ? var.repository : "gcr.io/${var.project_id}/${var.name}" } @@ -203,6 +217,22 @@ resource "google_service_account" "delivery" { display_name = "Dedicated service account for invoking ${google_cloud_run_v2_job.job.name}." } +module "audit-delivery-serviceaccount" { + count = length(var.notification_channels) > 0 ? 1 : 0 + + source = "../audit-serviceaccount" + + project_id = var.project_id + service-account = google_service_account.delivery.email + + # The absence of authorized identities here means that + # nothing is authorized to act as this service account. + # Note: Cloud Scheduler's usage doesn't show up in the + # audit logs. + + notification_channels = var.notification_channels +} + resource "google_cloud_run_v2_job_iam_binding" "authorize-calls" { project = google_cloud_run_v2_job.job.project location = google_cloud_run_v2_job.job.location @@ -252,6 +282,109 @@ data "google_project" "project" { project_id = var.project_id } // What identity is deploying this? data "google_client_openid_userinfo" "me" {} +// Create an alert policy to notify if the job is accessed by an unauthorized entity. +resource "google_monitoring_alert_policy" "anomalous-job-access" { + count = length(var.notification_channels) > 0 ? 1 : 0 + + # In the absence of data, incident will auto-close after an hour + alert_strategy { + auto_close = "3600s" + + notification_rate_limit { + period = "3600s" // re-alert hourly if condition still valid. + } + } + + display_name = "Abnormal CronJob Access: ${var.name}" + combiner = "OR" + + conditions { + display_name = "Abnormal CronJob Access: ${var.name}" + + condition_matched_log { + filter = < [audit-usage](#module\_audit-usage) | ../audit-serviceaccount | n/a | ## Resources diff --git a/modules/github-gsa/main.tf b/modules/github-gsa/main.tf index e661e3ad..33547c00 100644 --- a/modules/github-gsa/main.tf +++ b/modules/github-gsa/main.tf @@ -132,3 +132,17 @@ resource "google_service_account_iam_binding" "allow-impersonation" { } } } + +// Create an auditing policy to ensure that tokens are only issued for identities +// matching our expectations. +module "audit-usage" { + count = length(var.notification_channels) > 0 ? 1 : 0 + + source = "../audit-serviceaccount" + + project_id = var.project_id + service-account = google_service_account.this.email + + allowed_principal_regex = local.principalSubject + notification_channels = var.notification_channels +} diff --git a/modules/regional-go-service/main.tf b/modules/regional-go-service/main.tf index 068c892e..68f6a8c9 100644 --- a/modules/regional-go-service/main.tf +++ b/modules/regional-go-service/main.tf @@ -76,6 +76,11 @@ moved { to = module.this.google_cloud_run_v2_service.this } +moved { + from = google_monitoring_alert_policy.anomalous-service-access + to = module.this.google_monitoring_alert_policy.anomalous-service-access +} + moved { from = google_monitoring_alert_policy.bad-rollout to = module.this.google_monitoring_alert_policy.bad-rollout diff --git a/modules/regional-service/README.md b/modules/regional-service/README.md index 1c45d454..d732ef28 100644 --- a/modules/regional-service/README.md +++ b/modules/regional-service/README.md @@ -65,7 +65,9 @@ No requirements. ## Modules -No modules. +| Name | Source | Version | +|------|--------|---------| +| [audit-serviceaccount](#module\_audit-serviceaccount) | ../audit-serviceaccount | n/a | ## Resources @@ -73,6 +75,7 @@ No modules. |------|------| | [google-beta_google_cloud_run_v2_service.this](https://registry.terraform.io/providers/hashicorp/google-beta/latest/docs/resources/google_cloud_run_v2_service) | resource | | [google_cloud_run_v2_service_iam_member.public-services-are-unauthenticated](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/cloud_run_v2_service_iam_member) | resource | +| [google_monitoring_alert_policy.anomalous-service-access](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/monitoring_alert_policy) | resource | | [google_project_iam_member.metrics-writer](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/project_iam_member) | resource | | [google_project_iam_member.profiler-writer](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/project_iam_member) | resource | | [google_project_iam_member.trace-writer](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/project_iam_member) | resource | diff --git a/modules/regional-service/main.tf b/modules/regional-service/main.tf index e0b8a0ee..0cb60d94 100644 --- a/modules/regional-service/main.tf +++ b/modules/regional-service/main.tf @@ -1,3 +1,18 @@ +module "audit-serviceaccount" { + count = length(var.notification_channels) > 0 ? 1 : 0 + + source = "../audit-serviceaccount" + + project_id = var.project_id + service-account = var.service_account + + # The absence of authorized identities here means that + # nothing is authorized to act as this service account. + # Note: Cloud Run's usage doesn't show up in the audit logs. + + notification_channels = var.notification_channels +} + resource "google_project_iam_member" "metrics-writer" { project = var.project_id role = "roles/monitoring.metricWriter" @@ -267,6 +282,61 @@ data "google_project" "project" { project_id = var.project_id } // What identity is deploying this? data "google_client_openid_userinfo" "me" {} +// Create an alert policy to notify if the service is accessed by an unauthorized entity. +resource "google_monitoring_alert_policy" "anomalous-service-access" { + count = length(var.notification_channels) > 0 ? 1 : 0 + + # In the absence of data, incident will auto-close after an hour + alert_strategy { + auto_close = "3600s" + + notification_rate_limit { + period = "3600s" // re-alert hourly if condition still valid. + } + } + + display_name = "Abnormal Service Access: ${var.name}" + combiner = "OR" + + conditions { + display_name = "Abnormal Service Access: ${var.name}" + + condition_matched_log { + filter = < [authorized-adder](#input\_authorized-adder) | A member-style representation of the identity authorized to add new secret values (e.g. group:oncall@my-corp.dev). | `string` | n/a | yes | | [create\_placeholder\_version](#input\_create\_placeholder\_version) | Whether to create a placeholder secret version to avoid bad reference on first deploy. | `bool` | `false` | no | | [name](#input\_name) | The name to give the secret. | `string` | n/a | yes | -| [notification-channels](#input\_notification-channels) | The channels to notify if the configuration data is improperly accessed. | `list(string)` | n/a | yes | +| [notification\_channels](#input\_notification\_channels) | The channels to notify if the configuration data is improperly accessed. | `list(string)` | n/a | yes | | [project\_id](#input\_project\_id) | n/a | `string` | n/a | yes | | [service-account](#input\_service-account) | The email of the service account that will access the secret. | `string` | n/a | yes | diff --git a/modules/secret/main.tf b/modules/secret/main.tf index 3beb9ff4..c5a5167c 100644 --- a/modules/secret/main.tf +++ b/modules/secret/main.tf @@ -41,6 +41,8 @@ data "google_project" "project" { project_id = var.project_id } // Create an alert policy to notify if the secret is accessed by an unauthorized entity. resource "google_monitoring_alert_policy" "anomalous-secret-access" { + count = length(var.notification_channels) > 0 ? 1 : 0 + # In the absence of data, incident will auto-close after an hour alert_strategy { auto_close = "3600s" @@ -80,7 +82,7 @@ resource "google_monitoring_alert_policy" "anomalous-secret-access" { } } - notification_channels = var.notification-channels + notification_channels = var.notification_channels enabled = "true" project = var.project_id diff --git a/modules/secret/variables.tf b/modules/secret/variables.tf index 89c6a005..f0df212f 100644 --- a/modules/secret/variables.tf +++ b/modules/secret/variables.tf @@ -17,7 +17,7 @@ variable "service-account" { type = string } -variable "notification-channels" { +variable "notification_channels" { description = "The channels to notify if the configuration data is improperly accessed." type = list(string) } diff --git a/modules/serverless-gclb/README.md b/modules/serverless-gclb/README.md index 6daf836b..30696365 100644 --- a/modules/serverless-gclb/README.md +++ b/modules/serverless-gclb/README.md @@ -95,6 +95,7 @@ No modules. | [google_compute_target_https_proxy.public-service](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_target_https_proxy) | resource | | [google_compute_url_map.public-service](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_url_map) | resource | | [google_dns_record_set.public-service](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/dns_record_set) | resource | +| [google_monitoring_alert_policy.abnormal-gclb-access](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/monitoring_alert_policy) | resource | | [google_client_openid_userinfo.me](https://registry.terraform.io/providers/hashicorp/google/latest/docs/data-sources/client_openid_userinfo) | data source | ## Inputs diff --git a/modules/serverless-gclb/main.tf b/modules/serverless-gclb/main.tf index 030b395d..d385e35a 100644 --- a/modules/serverless-gclb/main.tf +++ b/modules/serverless-gclb/main.tf @@ -165,3 +165,47 @@ locals { ) } +resource "google_monitoring_alert_policy" "abnormal-gclb-access" { + count = length(var.notification_channels) > 0 ? 1 : 0 + + # In the absence of data, incident will auto-close after an hour + alert_strategy { + auto_close = "3600s" + + notification_rate_limit { + period = "3600s" // re-alert hourly if condition still valid. + } + } + + display_name = "Abnormal GCLB Access" + combiner = "OR" + + conditions { + display_name = "Anomaly detected" + + condition_matched_log { + filter = <