From f2b8c6d12cd19e22d17eef4dc1d85db6bdd07b58 Mon Sep 17 00:00:00 2001 From: Kirk Bater Date: Thu, 25 Jun 2026 14:09:46 -0400 Subject: [PATCH 1/6] Add Grafana ingress Terraform module with ALB, DNS, and ACM cert Internet-facing ALB in public subnets with TLS termination, Route53 alias record (grafana..), and security groups. Gated behind enable_grafana_ingress feature flag (enabled for integration). Cluster-side auth proxy will follow in a separate branch. Co-Authored-By: Claude Opus 4.6 --- config/defaults.yaml | 6 ++ config/integration/defaults.yaml | 2 + .../argocd-bootstrap/applicationset.yaml.j2 | 3 + .../terraform.json.j2 | 1 + .../ephemeral/us-east-1/_merged_config.yaml | 6 ++ .../applicationset.yaml | 3 + .../applicationset.yaml | 3 + .../terraform.json | 1 + .../integration/us-east-1/_merged_config.yaml | 6 ++ .../applicationset.yaml | 3 + .../applicationset.yaml | 3 + .../terraform.json | 1 + scripts/bootstrap-argocd.sh | 3 + terraform/config/regional-cluster/main.tf | 17 ++++ terraform/config/regional-cluster/outputs.tf | 9 ++ .../config/regional-cluster/variables.tf | 6 +- terraform/modules/ecs-bootstrap/main.tf | 1 + terraform/modules/grafana-ingress/acm.tf | 26 ++++++ terraform/modules/grafana-ingress/dns.tf | 11 +++ terraform/modules/grafana-ingress/main.tf | 86 +++++++++++++++++++ terraform/modules/grafana-ingress/outputs.tf | 14 +++ .../grafana-ingress/security-groups.tf | 47 ++++++++++ .../modules/grafana-ingress/variables.tf | 44 ++++++++++ terraform/modules/grafana-ingress/versions.tf | 10 +++ 24 files changed, 311 insertions(+), 1 deletion(-) create mode 100644 terraform/modules/grafana-ingress/acm.tf create mode 100644 terraform/modules/grafana-ingress/dns.tf create mode 100644 terraform/modules/grafana-ingress/main.tf create mode 100644 terraform/modules/grafana-ingress/outputs.tf create mode 100644 terraform/modules/grafana-ingress/security-groups.tf create mode 100644 terraform/modules/grafana-ingress/variables.tf create mode 100644 terraform/modules/grafana-ingress/versions.tf diff --git a/config/defaults.yaml b/config/defaults.yaml index 6411ea554..1c6646885 100644 --- a/config/defaults.yaml +++ b/config/defaults.yaml @@ -94,6 +94,12 @@ observability: # @used-by observability.pagerduty.escalation_policy_id pipeline-regional-cluster-inputs/terraform.json.j2 escalation_policy_id: "P5KE444" + grafana_ingress: + # @doc observability.grafana_ingress.enabled Enable internet-facing Grafana ingress (ALB + ACM cert + DNS record). + # @used-by observability.grafana_ingress.enabled _context + # @used-by observability.grafana_ingress.enabled pipeline-regional-cluster-inputs/terraform.json.j2 + enabled: false + sns_alerting: # @doc observability.sns_alerting.enabled Enable SNS alerting for regional cluster alert consumption by automated/external services. # @used-by observability.sns_alerting.enabled _context diff --git a/config/integration/defaults.yaml b/config/integration/defaults.yaml index ceba56048..5a104b3cc 100644 --- a/config/integration/defaults.yaml +++ b/config/integration/defaults.yaml @@ -15,3 +15,5 @@ management_cluster_defaults: observability: pagerduty: enabled: true + grafana_ingress: + enabled: true diff --git a/config/templates/argocd-bootstrap/applicationset.yaml.j2 b/config/templates/argocd-bootstrap/applicationset.yaml.j2 index a33d735aa..40113f570 100644 --- a/config/templates/argocd-bootstrap/applicationset.yaml.j2 +++ b/config/templates/argocd-bootstrap/applicationset.yaml.j2 @@ -80,6 +80,9 @@ spec: tableName: '{{ '{{ .metadata.annotations.zoa_table_name }}' }}' auditTableName: '{{ '{{ .metadata.annotations.zoa_audit_table_name }}' }}' bucketName: '{{ '{{ .metadata.annotations.zoa_bucket_name }}' }}' + grafana: + targetGroup: + arn: '{{ '{{ .metadata.annotations.grafana_target_group_arn }}' }}' # Thanos Operator values (keys must match chart's values.yaml structure) thanos: kmsKeyArn: '{{ '{{ .metadata.annotations.thanos_kms_key_arn }}' }}' diff --git a/config/templates/pipeline-regional-cluster-inputs/terraform.json.j2 b/config/templates/pipeline-regional-cluster-inputs/terraform.json.j2 index b7d2e027c..cb289f2d9 100644 --- a/config/templates/pipeline-regional-cluster-inputs/terraform.json.j2 +++ b/config/templates/pipeline-regional-cluster-inputs/terraform.json.j2 @@ -15,6 +15,7 @@ "enable_api_custom_domain": {{ regional_cluster.enable_api_custom_domain | default(false) | tojson }}, "zone_shard_count": {{ regional_cluster.zone_shard_count | default(1) | tojson }}, "enable_sns_alerting": {{ observability.sns_alerting.enabled | default(false) | tojson }}, + "enable_grafana_ingress": {{ observability.grafana_ingress.enabled | default(false) | tojson }}, "regional_id": "{% if eph_prefix %}{{ eph_prefix }}-regional{% else %}regional{% endif %}", "eph_prefix": "{{ eph_prefix | default('') }}", "iot_log_level": {{ applications['regional-cluster'].maestro.iotLogLevel | default('WARN') | tojson }} diff --git a/deploy/ephemeral/us-east-1/_merged_config.yaml b/deploy/ephemeral/us-east-1/_merged_config.yaml index 6ade70ed9..3d364e907 100644 --- a/deploy/ephemeral/us-east-1/_merged_config.yaml +++ b/deploy/ephemeral/us-east-1/_merged_config.yaml @@ -97,6 +97,12 @@ observability: # @used-by observability.pagerduty.escalation_policy_id pipeline-regional-cluster-inputs/terraform.json.j2 escalation_policy_id: "P5KE444" + grafana_ingress: + # @doc observability.grafana_ingress.enabled Enable internet-facing Grafana ingress (ALB + ACM cert + DNS record). + # @used-by observability.grafana_ingress.enabled _context + # @used-by observability.grafana_ingress.enabled pipeline-regional-cluster-inputs/terraform.json.j2 + enabled: false + sns_alerting: # @doc observability.sns_alerting.enabled Enable SNS alerting for regional cluster alert consumption by automated/external services. # @used-by observability.sns_alerting.enabled _context diff --git a/deploy/ephemeral/us-east-1/argocd-bootstrap-management-cluster/applicationset.yaml b/deploy/ephemeral/us-east-1/argocd-bootstrap-management-cluster/applicationset.yaml index 0899673e8..9ac761dea 100644 --- a/deploy/ephemeral/us-east-1/argocd-bootstrap-management-cluster/applicationset.yaml +++ b/deploy/ephemeral/us-east-1/argocd-bootstrap-management-cluster/applicationset.yaml @@ -79,6 +79,9 @@ spec: tableName: '{{ .metadata.annotations.zoa_table_name }}' auditTableName: '{{ .metadata.annotations.zoa_audit_table_name }}' bucketName: '{{ .metadata.annotations.zoa_bucket_name }}' + grafana: + targetGroup: + arn: '{{ .metadata.annotations.grafana_target_group_arn }}' # Thanos Operator values (keys must match chart's values.yaml structure) thanos: kmsKeyArn: '{{ .metadata.annotations.thanos_kms_key_arn }}' diff --git a/deploy/ephemeral/us-east-1/argocd-bootstrap-regional-cluster/applicationset.yaml b/deploy/ephemeral/us-east-1/argocd-bootstrap-regional-cluster/applicationset.yaml index 8ab03a7be..f6d4b6263 100644 --- a/deploy/ephemeral/us-east-1/argocd-bootstrap-regional-cluster/applicationset.yaml +++ b/deploy/ephemeral/us-east-1/argocd-bootstrap-regional-cluster/applicationset.yaml @@ -79,6 +79,9 @@ spec: tableName: '{{ .metadata.annotations.zoa_table_name }}' auditTableName: '{{ .metadata.annotations.zoa_audit_table_name }}' bucketName: '{{ .metadata.annotations.zoa_bucket_name }}' + grafana: + targetGroup: + arn: '{{ .metadata.annotations.grafana_target_group_arn }}' # Thanos Operator values (keys must match chart's values.yaml structure) thanos: kmsKeyArn: '{{ .metadata.annotations.thanos_kms_key_arn }}' diff --git a/deploy/ephemeral/us-east-1/pipeline-regional-cluster-inputs/terraform.json b/deploy/ephemeral/us-east-1/pipeline-regional-cluster-inputs/terraform.json index 311bb8d39..38116a5ba 100644 --- a/deploy/ephemeral/us-east-1/pipeline-regional-cluster-inputs/terraform.json +++ b/deploy/ephemeral/us-east-1/pipeline-regional-cluster-inputs/terraform.json @@ -15,6 +15,7 @@ "enable_api_custom_domain": false, "zone_shard_count": 1, "enable_sns_alerting": true, + "enable_grafana_ingress": false, "regional_id": "regional", "eph_prefix": "", "iot_log_level": "WARN", diff --git a/deploy/integration/us-east-1/_merged_config.yaml b/deploy/integration/us-east-1/_merged_config.yaml index bcdbdcebb..2c9c9ee38 100644 --- a/deploy/integration/us-east-1/_merged_config.yaml +++ b/deploy/integration/us-east-1/_merged_config.yaml @@ -97,6 +97,12 @@ observability: # @used-by observability.pagerduty.escalation_policy_id pipeline-regional-cluster-inputs/terraform.json.j2 escalation_policy_id: "P5KE444" + grafana_ingress: + # @doc observability.grafana_ingress.enabled Enable internet-facing Grafana ingress (ALB + ACM cert + DNS record). + # @used-by observability.grafana_ingress.enabled _context + # @used-by observability.grafana_ingress.enabled pipeline-regional-cluster-inputs/terraform.json.j2 + enabled: true + sns_alerting: # @doc observability.sns_alerting.enabled Enable SNS alerting for regional cluster alert consumption by automated/external services. # @used-by observability.sns_alerting.enabled _context diff --git a/deploy/integration/us-east-1/argocd-bootstrap-management-cluster/applicationset.yaml b/deploy/integration/us-east-1/argocd-bootstrap-management-cluster/applicationset.yaml index c4d7c16a5..d12c3e6d5 100644 --- a/deploy/integration/us-east-1/argocd-bootstrap-management-cluster/applicationset.yaml +++ b/deploy/integration/us-east-1/argocd-bootstrap-management-cluster/applicationset.yaml @@ -79,6 +79,9 @@ spec: tableName: '{{ .metadata.annotations.zoa_table_name }}' auditTableName: '{{ .metadata.annotations.zoa_audit_table_name }}' bucketName: '{{ .metadata.annotations.zoa_bucket_name }}' + grafana: + targetGroup: + arn: '{{ .metadata.annotations.grafana_target_group_arn }}' # Thanos Operator values (keys must match chart's values.yaml structure) thanos: kmsKeyArn: '{{ .metadata.annotations.thanos_kms_key_arn }}' diff --git a/deploy/integration/us-east-1/argocd-bootstrap-regional-cluster/applicationset.yaml b/deploy/integration/us-east-1/argocd-bootstrap-regional-cluster/applicationset.yaml index 50b2adec9..e596f1c38 100644 --- a/deploy/integration/us-east-1/argocd-bootstrap-regional-cluster/applicationset.yaml +++ b/deploy/integration/us-east-1/argocd-bootstrap-regional-cluster/applicationset.yaml @@ -79,6 +79,9 @@ spec: tableName: '{{ .metadata.annotations.zoa_table_name }}' auditTableName: '{{ .metadata.annotations.zoa_audit_table_name }}' bucketName: '{{ .metadata.annotations.zoa_bucket_name }}' + grafana: + targetGroup: + arn: '{{ .metadata.annotations.grafana_target_group_arn }}' # Thanos Operator values (keys must match chart's values.yaml structure) thanos: kmsKeyArn: '{{ .metadata.annotations.thanos_kms_key_arn }}' diff --git a/deploy/integration/us-east-1/pipeline-regional-cluster-inputs/terraform.json b/deploy/integration/us-east-1/pipeline-regional-cluster-inputs/terraform.json index 94f837d26..db25703c3 100644 --- a/deploy/integration/us-east-1/pipeline-regional-cluster-inputs/terraform.json +++ b/deploy/integration/us-east-1/pipeline-regional-cluster-inputs/terraform.json @@ -15,6 +15,7 @@ "enable_api_custom_domain": true, "zone_shard_count": 1, "enable_sns_alerting": true, + "enable_grafana_ingress": true, "regional_id": "regional", "eph_prefix": "", "iot_log_level": "WARN", diff --git a/scripts/bootstrap-argocd.sh b/scripts/bootstrap-argocd.sh index 5e82e9015..1a3ff4aee 100755 --- a/scripts/bootstrap-argocd.sh +++ b/scripts/bootstrap-argocd.sh @@ -71,6 +71,7 @@ if [[ "$CLUSTER_TYPE" == "regional-cluster" ]]; then LOKI_KMS_KEY_ARN=$(echo "$OUTPUTS" | jq -r '.loki_kms_key_arn.value // ""') LOKI_DISTRIBUTOR_TARGET_GROUP_ARN=$(echo "$OUTPUTS" | jq -r '.loki_distributor_target_group_arn.value // ""') LOKI_QUERY_FRONTEND_TARGET_GROUP_ARN=$(echo "$OUTPUTS" | jq -r '.loki_query_frontend_target_group_arn.value // ""') + GRAFANA_TARGET_GROUP_ARN=$(echo "$OUTPUTS" | jq -r '.grafana_target_group_arn.value // ""') ZOA_TABLE_NAME=$(echo "$OUTPUTS" | jq -r '.zoa_table_name.value // ""') ZOA_AUDIT_TABLE_NAME=$(echo "$OUTPUTS" | jq -r '.zoa_audit_table_name.value // ""') ZOA_BUCKET_NAME=$(echo "$OUTPUTS" | jq -r '.zoa_bucket_name.value // ""') @@ -81,6 +82,7 @@ else LOKI_KMS_KEY_ARN="" LOKI_DISTRIBUTOR_TARGET_GROUP_ARN="" LOKI_QUERY_FRONTEND_TARGET_GROUP_ARN="" + GRAFANA_TARGET_GROUP_ARN="" ZOA_TABLE_NAME="" ZOA_AUDIT_TABLE_NAME="" ZOA_BUCKET_NAME="" @@ -115,6 +117,7 @@ RUN_TASK_OUTPUT=$(aws ecs run-task \ {\"name\": \"LOKI_KMS_KEY_ARN\", \"value\": \"$LOKI_KMS_KEY_ARN\"}, {\"name\": \"LOKI_DISTRIBUTOR_TARGET_GROUP_ARN\", \"value\": \"$LOKI_DISTRIBUTOR_TARGET_GROUP_ARN\"}, {\"name\": \"LOKI_QUERY_FRONTEND_TARGET_GROUP_ARN\", \"value\": \"$LOKI_QUERY_FRONTEND_TARGET_GROUP_ARN\"}, + {\"name\": \"GRAFANA_TARGET_GROUP_ARN\", \"value\": \"$GRAFANA_TARGET_GROUP_ARN\"}, {\"name\": \"RHOBS_API_URL\", \"value\": \"$RHOBS_API_URL\"}, {\"name\": \"DNS_ZONE_OPERATOR_ROLE_ARN\", \"value\": \"$DNS_ZONE_OPERATOR_ROLE_ARN\"}, {\"name\": \"ZOA_TABLE_NAME\", \"value\": \"$ZOA_TABLE_NAME\"}, diff --git a/terraform/config/regional-cluster/main.tf b/terraform/config/regional-cluster/main.tf index 71a05a5bf..ce4ece116 100755 --- a/terraform/config/regional-cluster/main.tf +++ b/terraform/config/regional-cluster/main.tf @@ -504,6 +504,23 @@ module "pagerduty_service" { escalation_policy_id = var.pagerduty_escalation_policy_id } +# ============================================================================= +# Grafana Ingress (Optional) - Internet-facing ALB for Grafana +# ============================================================================= + +module "grafana_ingress" { + count = var.enable_grafana_ingress && var.environment_domain != null ? 1 : 0 + source = "../../modules/grafana-ingress" + + regional_id = var.regional_id + vpc_id = module.vpc.vpc_id + public_subnet_ids = module.vpc.public_subnet_ids + node_security_group_id = module.regional_cluster.node_security_group_id + cluster_name = module.regional_cluster.cluster_name + domain_name = "grafana.${var.deployment_name}.${var.environment_domain}" + regional_hosted_zone_id = aws_route53_zone.regional[0].zone_id +} + # ============================================================================= # SNS Alerting Module (Phase 2 Alert Fan-Out) # ============================================================================= diff --git a/terraform/config/regional-cluster/outputs.tf b/terraform/config/regional-cluster/outputs.tf index 1aafbdfa2..19897db33 100755 --- a/terraform/config/regional-cluster/outputs.tf +++ b/terraform/config/regional-cluster/outputs.tf @@ -471,3 +471,12 @@ output "zoa_kms_key_arn" { description = "KMS key ARN for ZOA encryption (used by MC Pod Identity for S3 SSE-KMS)" value = module.zoa.kms_key_arn } + +# ============================================================================= +# Grafana Ingress Outputs +# ============================================================================= + +output "grafana_target_group_arn" { + description = "Target group ARN for Grafana TargetGroupBinding" + value = var.enable_grafana_ingress && var.environment_domain != null ? module.grafana_ingress[0].target_group_arn : null +} diff --git a/terraform/config/regional-cluster/variables.tf b/terraform/config/regional-cluster/variables.tf index c8fc2c366..26c678752 100755 --- a/terraform/config/regional-cluster/variables.tf +++ b/terraform/config/regional-cluster/variables.tf @@ -111,7 +111,11 @@ variable "enable_sns_alerting" { default = false } - +variable "enable_grafana_ingress" { + description = "Enable internet-facing Grafana ingress with ALB, ACM cert, and DNS" + type = bool + default = false +} # ============================================================================= # Platform API Variables diff --git a/terraform/modules/ecs-bootstrap/main.tf b/terraform/modules/ecs-bootstrap/main.tf index 365d55e6a..397145e77 100644 --- a/terraform/modules/ecs-bootstrap/main.tf +++ b/terraform/modules/ecs-bootstrap/main.tf @@ -210,6 +210,7 @@ resource "aws_ecs_task_definition" "bootstrap" { loki_kms_key_arn: "$LOKI_KMS_KEY_ARN" loki_distributor_target_group_arn: "$LOKI_DISTRIBUTOR_TARGET_GROUP_ARN" loki_query_frontend_target_group_arn: "$LOKI_QUERY_FRONTEND_TARGET_GROUP_ARN" + grafana_target_group_arn: "$GRAFANA_TARGET_GROUP_ARN" aws_account_id: "$AWS_ACCOUNT_ID" management_clusters: "$MANAGEMENT_CLUSTERS" rhobs_api_url: "$RHOBS_API_URL" diff --git a/terraform/modules/grafana-ingress/acm.tf b/terraform/modules/grafana-ingress/acm.tf new file mode 100644 index 000000000..48a9d3f2b --- /dev/null +++ b/terraform/modules/grafana-ingress/acm.tf @@ -0,0 +1,26 @@ +resource "aws_acm_certificate" "grafana" { + domain_name = var.domain_name + validation_method = "DNS" + + tags = { + Name = "${var.regional_id}-grafana-cert" + } + + lifecycle { + create_before_destroy = true + } +} + +resource "aws_route53_record" "cert_validation" { + zone_id = var.regional_hosted_zone_id + name = tolist(aws_acm_certificate.grafana.domain_validation_options)[0].resource_record_name + type = tolist(aws_acm_certificate.grafana.domain_validation_options)[0].resource_record_type + ttl = 300 + records = [tolist(aws_acm_certificate.grafana.domain_validation_options)[0].resource_record_value] + allow_overwrite = true +} + +resource "aws_acm_certificate_validation" "grafana" { + certificate_arn = aws_acm_certificate.grafana.arn + validation_record_fqdns = [aws_route53_record.cert_validation.fqdn] +} diff --git a/terraform/modules/grafana-ingress/dns.tf b/terraform/modules/grafana-ingress/dns.tf new file mode 100644 index 000000000..5b5900d97 --- /dev/null +++ b/terraform/modules/grafana-ingress/dns.tf @@ -0,0 +1,11 @@ +resource "aws_route53_record" "grafana" { + zone_id = var.regional_hosted_zone_id + name = var.domain_name + type = "A" + + alias { + name = aws_lb.grafana.dns_name + zone_id = aws_lb.grafana.zone_id + evaluate_target_health = true + } +} diff --git a/terraform/modules/grafana-ingress/main.tf b/terraform/modules/grafana-ingress/main.tf new file mode 100644 index 000000000..6cd1e6d3c --- /dev/null +++ b/terraform/modules/grafana-ingress/main.tf @@ -0,0 +1,86 @@ +data "aws_region" "current" {} + +# ----------------------------------------------------------------------------- +# Application Load Balancer (internet-facing) +# ----------------------------------------------------------------------------- + +resource "aws_lb" "grafana" { + name = "${var.regional_id}-grafana" + internal = false + load_balancer_type = "application" + security_groups = [aws_security_group.alb.id] + subnets = var.public_subnet_ids + + tags = { + Name = "${var.regional_id}-grafana" + } +} + +# ----------------------------------------------------------------------------- +# Target Group +# +# Uses IP target type for TargetGroupBinding compatibility. +# The eks:eks-cluster-name tag is REQUIRED for EKS Auto Mode — +# AmazonEKSLoadBalancingPolicy only allows RegisterTargets on tagged TGs. +# ----------------------------------------------------------------------------- + +resource "aws_lb_target_group" "grafana" { + name = "${var.regional_id}-grafana" + port = 4180 + protocol = "HTTP" + vpc_id = var.vpc_id + target_type = "ip" + + health_check { + enabled = true + path = "/ping" + port = "traffic-port" + protocol = "HTTP" + healthy_threshold = 2 + unhealthy_threshold = 3 + timeout = 5 + interval = 30 + matcher = "200" + } + + tags = { + Name = "${var.regional_id}-grafana" + "eks:eks-cluster-name" = var.cluster_name + } +} + +# ----------------------------------------------------------------------------- +# HTTPS Listener (443) — terminates TLS with ACM certificate +# ----------------------------------------------------------------------------- + +resource "aws_lb_listener" "https" { + load_balancer_arn = aws_lb.grafana.arn + port = 443 + protocol = "HTTPS" + ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06" + certificate_arn = aws_acm_certificate_validation.grafana.certificate_arn + + default_action { + type = "forward" + target_group_arn = aws_lb_target_group.grafana.arn + } +} + +# ----------------------------------------------------------------------------- +# HTTP Listener (80) — redirects to HTTPS +# ----------------------------------------------------------------------------- + +resource "aws_lb_listener" "http_redirect" { + load_balancer_arn = aws_lb.grafana.arn + port = 80 + protocol = "HTTP" + + default_action { + type = "redirect" + redirect { + port = "443" + protocol = "HTTPS" + status_code = "HTTP_301" + } + } +} diff --git a/terraform/modules/grafana-ingress/outputs.tf b/terraform/modules/grafana-ingress/outputs.tf new file mode 100644 index 000000000..07e705d4c --- /dev/null +++ b/terraform/modules/grafana-ingress/outputs.tf @@ -0,0 +1,14 @@ +output "target_group_arn" { + description = "Target group ARN for TargetGroupBinding in Kubernetes" + value = aws_lb_target_group.grafana.arn +} + +output "grafana_domain" { + description = "Grafana FQDN (e.g. grafana.us-east-1.int0.rosa.devshift.net)" + value = var.domain_name +} + +output "alb_dns_name" { + description = "ALB DNS name" + value = aws_lb.grafana.dns_name +} diff --git a/terraform/modules/grafana-ingress/security-groups.tf b/terraform/modules/grafana-ingress/security-groups.tf new file mode 100644 index 000000000..a6c1e6690 --- /dev/null +++ b/terraform/modules/grafana-ingress/security-groups.tf @@ -0,0 +1,47 @@ +resource "aws_security_group" "alb" { + name = "${var.regional_id}-grafana-alb" + description = "Security group for internet-facing Grafana ALB" + vpc_id = var.vpc_id + + revoke_rules_on_delete = false + + tags = { + Name = "${var.regional_id}-grafana-alb" + } +} + +resource "aws_vpc_security_group_ingress_rule" "alb_https" { + security_group_id = aws_security_group.alb.id + description = "Allow HTTPS from internet" + ip_protocol = "tcp" + from_port = 443 + to_port = 443 + cidr_ipv4 = "0.0.0.0/0" +} + +resource "aws_vpc_security_group_ingress_rule" "alb_http" { + security_group_id = aws_security_group.alb.id + description = "Allow HTTP from internet (redirected to HTTPS)" + ip_protocol = "tcp" + from_port = 80 + to_port = 80 + cidr_ipv4 = "0.0.0.0/0" +} + +resource "aws_vpc_security_group_egress_rule" "alb_to_targets" { + security_group_id = aws_security_group.alb.id + description = "Allow traffic to oauth2-proxy pods" + ip_protocol = "tcp" + from_port = 4180 + to_port = 4180 + referenced_security_group_id = var.node_security_group_id +} + +resource "aws_vpc_security_group_ingress_rule" "nodes_from_alb" { + security_group_id = var.node_security_group_id + description = "Allow Grafana ALB traffic to oauth2-proxy pods" + ip_protocol = "tcp" + from_port = 4180 + to_port = 4180 + referenced_security_group_id = aws_security_group.alb.id +} diff --git a/terraform/modules/grafana-ingress/variables.tf b/terraform/modules/grafana-ingress/variables.tf new file mode 100644 index 000000000..9dcfea167 --- /dev/null +++ b/terraform/modules/grafana-ingress/variables.tf @@ -0,0 +1,44 @@ +variable "regional_id" { + description = "Regional cluster identifier for resource naming" + type = string +} + +variable "vpc_id" { + description = "VPC ID where the ALB will be created" + type = string +} + +variable "public_subnet_ids" { + description = "List of public subnet IDs for internet-facing ALB placement" + type = list(string) + + validation { + condition = length(var.public_subnet_ids) >= 2 + error_message = "At least 2 public subnets are required for ALB high availability." + } +} + +variable "node_security_group_id" { + description = "EKS node/pod security group ID. For EKS Auto Mode, use the cluster_primary_security_group_id." + type = string +} + +variable "cluster_name" { + description = "EKS cluster name — required for tagging target group with eks:eks-cluster-name for Auto Mode IAM permissions" + type = string +} + +variable "domain_name" { + description = "FQDN for the Grafana endpoint (e.g. grafana.us-east-1.int0.rosa.devshift.net)" + type = string + + validation { + condition = can(regex("^[a-z0-9][a-z0-9.-]+[a-z0-9]$", var.domain_name)) + error_message = "domain_name must be a valid domain name." + } +} + +variable "regional_hosted_zone_id" { + description = "Route53 hosted zone ID for the regional domain. Used for ACM DNS validation and the Grafana alias record." + type = string +} diff --git a/terraform/modules/grafana-ingress/versions.tf b/terraform/modules/grafana-ingress/versions.tf new file mode 100644 index 000000000..3dccf26c7 --- /dev/null +++ b/terraform/modules/grafana-ingress/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.14.3" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 6.0" + } + } +} From 735b8c320d38a82be70c8cfb8171a37538d0db6d Mon Sep 17 00:00:00 2001 From: Kirk Bater Date: Thu, 25 Jun 2026 14:16:14 -0400 Subject: [PATCH 2/6] Update Grafana ingress to target Grafana directly on port 3000 Using native Grafana OIDC auth instead of oauth2-proxy, so the ALB targets Grafana pods directly. Changes target group port from 4180 to 3000, health check from /ping to /api/health, and updates security group descriptions. Co-Authored-By: Claude Opus 4.6 --- terraform/modules/grafana-ingress/main.tf | 4 ++-- terraform/modules/grafana-ingress/security-groups.tf | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/terraform/modules/grafana-ingress/main.tf b/terraform/modules/grafana-ingress/main.tf index 6cd1e6d3c..89c348d60 100644 --- a/terraform/modules/grafana-ingress/main.tf +++ b/terraform/modules/grafana-ingress/main.tf @@ -26,14 +26,14 @@ resource "aws_lb" "grafana" { resource "aws_lb_target_group" "grafana" { name = "${var.regional_id}-grafana" - port = 4180 + port = 3000 protocol = "HTTP" vpc_id = var.vpc_id target_type = "ip" health_check { enabled = true - path = "/ping" + path = "/api/health" port = "traffic-port" protocol = "HTTP" healthy_threshold = 2 diff --git a/terraform/modules/grafana-ingress/security-groups.tf b/terraform/modules/grafana-ingress/security-groups.tf index a6c1e6690..b9dc0fd1f 100644 --- a/terraform/modules/grafana-ingress/security-groups.tf +++ b/terraform/modules/grafana-ingress/security-groups.tf @@ -30,18 +30,18 @@ resource "aws_vpc_security_group_ingress_rule" "alb_http" { resource "aws_vpc_security_group_egress_rule" "alb_to_targets" { security_group_id = aws_security_group.alb.id - description = "Allow traffic to oauth2-proxy pods" + description = "Allow traffic to Grafana pods" ip_protocol = "tcp" - from_port = 4180 - to_port = 4180 + from_port = 3000 + to_port = 3000 referenced_security_group_id = var.node_security_group_id } resource "aws_vpc_security_group_ingress_rule" "nodes_from_alb" { security_group_id = var.node_security_group_id - description = "Allow Grafana ALB traffic to oauth2-proxy pods" + description = "Allow Grafana ALB traffic to Grafana pods" ip_protocol = "tcp" - from_port = 4180 - to_port = 4180 + from_port = 3000 + to_port = 3000 referenced_security_group_id = aws_security_group.alb.id } From 88f7f8cf1c58455f85177f650ec9c26e40b55884 Mon Sep 17 00:00:00 2001 From: Kirk Bater Date: Thu, 25 Jun 2026 14:32:27 -0400 Subject: [PATCH 3/6] adds cross-account secret gathering and updates granafa ingress --- .../templates/sso-external-secret.yaml | 28 +++++++++++++++ .../grafana/templates/targetgroupbinding.yaml | 14 ++++++++ .../regional-cluster/grafana/values.yaml | 36 ++++++++++++++++--- .../argocd-bootstrap/applicationset.yaml.j2 | 2 ++ .../applicationset.yaml | 2 ++ .../applicationset.yaml | 2 ++ .../applicationset.yaml | 2 ++ .../applicationset.yaml | 2 ++ scripts/bootstrap-argocd.sh | 3 ++ terraform/config/regional-cluster/main.tf | 28 +++++++++++++++ terraform/config/regional-cluster/outputs.tf | 5 +++ terraform/modules/ecs-bootstrap/main.tf | 1 + 12 files changed, 120 insertions(+), 5 deletions(-) create mode 100644 argocd/config/regional-cluster/grafana/templates/sso-external-secret.yaml create mode 100644 argocd/config/regional-cluster/grafana/templates/targetgroupbinding.yaml diff --git a/argocd/config/regional-cluster/grafana/templates/sso-external-secret.yaml b/argocd/config/regional-cluster/grafana/templates/sso-external-secret.yaml new file mode 100644 index 000000000..0c552f03e --- /dev/null +++ b/argocd/config/regional-cluster/grafana/templates/sso-external-secret.yaml @@ -0,0 +1,28 @@ +{{- if .Values.grafana.sso.secretArn }} +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: grafana-sso + namespace: {{ .Release.Namespace }} +spec: + refreshInterval: 1h + secretStoreRef: + name: aws-secrets-manager + kind: ClusterSecretStore + target: + name: grafana-sso + creationPolicy: Owner + data: + - secretKey: client-id + remoteRef: + key: {{ .Values.grafana.sso.secretArn }} + property: client_id + - secretKey: client-secret + remoteRef: + key: {{ .Values.grafana.sso.secretArn }} + property: client_secret + - secretKey: oidc-url + remoteRef: + key: {{ .Values.grafana.sso.secretArn }} + property: oidc_url +{{- end }} diff --git a/argocd/config/regional-cluster/grafana/templates/targetgroupbinding.yaml b/argocd/config/regional-cluster/grafana/templates/targetgroupbinding.yaml new file mode 100644 index 000000000..e7e95dbe4 --- /dev/null +++ b/argocd/config/regional-cluster/grafana/templates/targetgroupbinding.yaml @@ -0,0 +1,14 @@ +{{- if .Values.grafana.targetGroup.arn }} +--- +apiVersion: eks.amazonaws.com/v1 +kind: TargetGroupBinding +metadata: + name: grafana + namespace: {{ .Release.Namespace }} +spec: + serviceRef: + name: grafana + port: 80 + targetGroupARN: {{ .Values.grafana.targetGroup.arn }} + targetType: ip +{{- end }} diff --git a/argocd/config/regional-cluster/grafana/values.yaml b/argocd/config/regional-cluster/grafana/values.yaml index 16ee80144..5265d6e30 100644 --- a/argocd/config/regional-cluster/grafana/values.yaml +++ b/argocd/config/regional-cluster/grafana/values.yaml @@ -36,9 +36,13 @@ grafana: create: true name: grafana + envFromSecrets: + - name: grafana-sso + optional: true + "grafana.ini": server: - root_url: "%(protocol)s://%(domain)s:%(http_port)s/" + root_url: "https://%(domain)s/" security: disable_gravatar: true @@ -49,17 +53,32 @@ grafana: wal: true auth: - disable_login_form: true + disable_login_form: false "auth.anonymous": - enabled: true - org_role: Admin + enabled: false "auth.basic": enabled: false + "auth.generic_oauth": + enabled: true + name: Red Hat SSO + allow_sign_up: true + client_id: $__env{client-id} + client_secret: $__env{client-secret} + scopes: openid email profile offline_access + email_attribute_path: email + login_attribute_path: preferred_username + name_attribute_path: name + auth_url: $__env{oidc-url}/protocol/openid-connect/auth + token_url: $__env{oidc-url}/protocol/openid-connect/token + api_url: $__env{oidc-url}/protocol/openid-connect/userinfo + skip_org_role_sync: true + auto_login: true + users: - allow_sign_up: false + allow_sign_up: true auto_assign_org_role: Admin analytics: @@ -179,3 +198,10 @@ grafana: provider: foldersFromFilesStructure: true allowUiUpdates: false + + # Injected at runtime by ApplicationSet from cluster annotations. + # Empty defaults for helm lint. + targetGroup: + arn: "" + sso: + secretArn: "" diff --git a/config/templates/argocd-bootstrap/applicationset.yaml.j2 b/config/templates/argocd-bootstrap/applicationset.yaml.j2 index 40113f570..de5a6fb6c 100644 --- a/config/templates/argocd-bootstrap/applicationset.yaml.j2 +++ b/config/templates/argocd-bootstrap/applicationset.yaml.j2 @@ -83,6 +83,8 @@ spec: grafana: targetGroup: arn: '{{ '{{ .metadata.annotations.grafana_target_group_arn }}' }}' + sso: + secretArn: '{{ '{{ .metadata.annotations.grafana_sso_secret_arn }}' }}' # Thanos Operator values (keys must match chart's values.yaml structure) thanos: kmsKeyArn: '{{ '{{ .metadata.annotations.thanos_kms_key_arn }}' }}' diff --git a/deploy/ephemeral/us-east-1/argocd-bootstrap-management-cluster/applicationset.yaml b/deploy/ephemeral/us-east-1/argocd-bootstrap-management-cluster/applicationset.yaml index 9ac761dea..e65e05b23 100644 --- a/deploy/ephemeral/us-east-1/argocd-bootstrap-management-cluster/applicationset.yaml +++ b/deploy/ephemeral/us-east-1/argocd-bootstrap-management-cluster/applicationset.yaml @@ -82,6 +82,8 @@ spec: grafana: targetGroup: arn: '{{ .metadata.annotations.grafana_target_group_arn }}' + sso: + secretArn: '{{ .metadata.annotations.grafana_sso_secret_arn }}' # Thanos Operator values (keys must match chart's values.yaml structure) thanos: kmsKeyArn: '{{ .metadata.annotations.thanos_kms_key_arn }}' diff --git a/deploy/ephemeral/us-east-1/argocd-bootstrap-regional-cluster/applicationset.yaml b/deploy/ephemeral/us-east-1/argocd-bootstrap-regional-cluster/applicationset.yaml index f6d4b6263..8c1eee837 100644 --- a/deploy/ephemeral/us-east-1/argocd-bootstrap-regional-cluster/applicationset.yaml +++ b/deploy/ephemeral/us-east-1/argocd-bootstrap-regional-cluster/applicationset.yaml @@ -82,6 +82,8 @@ spec: grafana: targetGroup: arn: '{{ .metadata.annotations.grafana_target_group_arn }}' + sso: + secretArn: '{{ .metadata.annotations.grafana_sso_secret_arn }}' # Thanos Operator values (keys must match chart's values.yaml structure) thanos: kmsKeyArn: '{{ .metadata.annotations.thanos_kms_key_arn }}' diff --git a/deploy/integration/us-east-1/argocd-bootstrap-management-cluster/applicationset.yaml b/deploy/integration/us-east-1/argocd-bootstrap-management-cluster/applicationset.yaml index d12c3e6d5..988f35601 100644 --- a/deploy/integration/us-east-1/argocd-bootstrap-management-cluster/applicationset.yaml +++ b/deploy/integration/us-east-1/argocd-bootstrap-management-cluster/applicationset.yaml @@ -82,6 +82,8 @@ spec: grafana: targetGroup: arn: '{{ .metadata.annotations.grafana_target_group_arn }}' + sso: + secretArn: '{{ .metadata.annotations.grafana_sso_secret_arn }}' # Thanos Operator values (keys must match chart's values.yaml structure) thanos: kmsKeyArn: '{{ .metadata.annotations.thanos_kms_key_arn }}' diff --git a/deploy/integration/us-east-1/argocd-bootstrap-regional-cluster/applicationset.yaml b/deploy/integration/us-east-1/argocd-bootstrap-regional-cluster/applicationset.yaml index e596f1c38..c36f8a4ce 100644 --- a/deploy/integration/us-east-1/argocd-bootstrap-regional-cluster/applicationset.yaml +++ b/deploy/integration/us-east-1/argocd-bootstrap-regional-cluster/applicationset.yaml @@ -82,6 +82,8 @@ spec: grafana: targetGroup: arn: '{{ .metadata.annotations.grafana_target_group_arn }}' + sso: + secretArn: '{{ .metadata.annotations.grafana_sso_secret_arn }}' # Thanos Operator values (keys must match chart's values.yaml structure) thanos: kmsKeyArn: '{{ .metadata.annotations.thanos_kms_key_arn }}' diff --git a/scripts/bootstrap-argocd.sh b/scripts/bootstrap-argocd.sh index 1a3ff4aee..f57721d73 100755 --- a/scripts/bootstrap-argocd.sh +++ b/scripts/bootstrap-argocd.sh @@ -72,6 +72,7 @@ if [[ "$CLUSTER_TYPE" == "regional-cluster" ]]; then LOKI_DISTRIBUTOR_TARGET_GROUP_ARN=$(echo "$OUTPUTS" | jq -r '.loki_distributor_target_group_arn.value // ""') LOKI_QUERY_FRONTEND_TARGET_GROUP_ARN=$(echo "$OUTPUTS" | jq -r '.loki_query_frontend_target_group_arn.value // ""') GRAFANA_TARGET_GROUP_ARN=$(echo "$OUTPUTS" | jq -r '.grafana_target_group_arn.value // ""') + GRAFANA_SSO_SECRET_ARN=$(echo "$OUTPUTS" | jq -r '.grafana_sso_secret_arn.value // ""') ZOA_TABLE_NAME=$(echo "$OUTPUTS" | jq -r '.zoa_table_name.value // ""') ZOA_AUDIT_TABLE_NAME=$(echo "$OUTPUTS" | jq -r '.zoa_audit_table_name.value // ""') ZOA_BUCKET_NAME=$(echo "$OUTPUTS" | jq -r '.zoa_bucket_name.value // ""') @@ -83,6 +84,7 @@ else LOKI_DISTRIBUTOR_TARGET_GROUP_ARN="" LOKI_QUERY_FRONTEND_TARGET_GROUP_ARN="" GRAFANA_TARGET_GROUP_ARN="" + GRAFANA_SSO_SECRET_ARN="" ZOA_TABLE_NAME="" ZOA_AUDIT_TABLE_NAME="" ZOA_BUCKET_NAME="" @@ -118,6 +120,7 @@ RUN_TASK_OUTPUT=$(aws ecs run-task \ {\"name\": \"LOKI_DISTRIBUTOR_TARGET_GROUP_ARN\", \"value\": \"$LOKI_DISTRIBUTOR_TARGET_GROUP_ARN\"}, {\"name\": \"LOKI_QUERY_FRONTEND_TARGET_GROUP_ARN\", \"value\": \"$LOKI_QUERY_FRONTEND_TARGET_GROUP_ARN\"}, {\"name\": \"GRAFANA_TARGET_GROUP_ARN\", \"value\": \"$GRAFANA_TARGET_GROUP_ARN\"}, + {\"name\": \"GRAFANA_SSO_SECRET_ARN\", \"value\": \"$GRAFANA_SSO_SECRET_ARN\"}, {\"name\": \"RHOBS_API_URL\", \"value\": \"$RHOBS_API_URL\"}, {\"name\": \"DNS_ZONE_OPERATOR_ROLE_ARN\", \"value\": \"$DNS_ZONE_OPERATOR_ROLE_ARN\"}, {\"name\": \"ZOA_TABLE_NAME\", \"value\": \"$ZOA_TABLE_NAME\"}, diff --git a/terraform/config/regional-cluster/main.tf b/terraform/config/regional-cluster/main.tf index ce4ece116..d09c9b367 100755 --- a/terraform/config/regional-cluster/main.tf +++ b/terraform/config/regional-cluster/main.tf @@ -51,6 +51,9 @@ provider "pagerduty" { # ============================================================================= data "aws_caller_identity" "current" {} +data "aws_caller_identity" "central" { + provider = aws.central +} locals { mc_entries = var.management_clusters != "" ? split(",", var.management_clusters) : [] @@ -129,6 +132,31 @@ resource "aws_iam_role_policy" "eso_secretsmanager" { }) } +resource "aws_iam_role_policy" "eso_central_secretsmanager" { + count = var.enable_grafana_ingress ? 1 : 0 + name = "${var.regional_id}-eso-central-secretsmanager" + role = aws_iam_role.external_secrets_operator.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Effect = "Allow" + Action = [ + "secretsmanager:GetSecretValue", + "secretsmanager:DescribeSecret" + ] + Resource = "arn:aws:secretsmanager:${var.region}:${data.aws_caller_identity.central.account_id}:secret:sso/rh-internal-*" + }] + }) +} + +resource "aws_ssm_parameter" "grafana_sso_secret_arn" { + count = var.enable_grafana_ingress ? 1 : 0 + name = "/${var.regional_id}/sso/rh-internal-secret-arn" + type = "String" + value = "arn:aws:secretsmanager:${var.region}:${data.aws_caller_identity.central.account_id}:secret:sso/rh-internal" +} + resource "aws_eks_pod_identity_association" "external_secrets_operator" { cluster_name = module.regional_cluster.cluster_name namespace = "external-secrets" diff --git a/terraform/config/regional-cluster/outputs.tf b/terraform/config/regional-cluster/outputs.tf index 19897db33..7eee86a43 100755 --- a/terraform/config/regional-cluster/outputs.tf +++ b/terraform/config/regional-cluster/outputs.tf @@ -480,3 +480,8 @@ output "grafana_target_group_arn" { description = "Target group ARN for Grafana TargetGroupBinding" value = var.enable_grafana_ingress && var.environment_domain != null ? module.grafana_ingress[0].target_group_arn : null } + +output "grafana_sso_secret_arn" { + description = "ARN of the central account Secrets Manager secret for Grafana SSO credentials" + value = var.enable_grafana_ingress ? aws_ssm_parameter.grafana_sso_secret_arn[0].value : null +} diff --git a/terraform/modules/ecs-bootstrap/main.tf b/terraform/modules/ecs-bootstrap/main.tf index 397145e77..f550168d1 100644 --- a/terraform/modules/ecs-bootstrap/main.tf +++ b/terraform/modules/ecs-bootstrap/main.tf @@ -211,6 +211,7 @@ resource "aws_ecs_task_definition" "bootstrap" { loki_distributor_target_group_arn: "$LOKI_DISTRIBUTOR_TARGET_GROUP_ARN" loki_query_frontend_target_group_arn: "$LOKI_QUERY_FRONTEND_TARGET_GROUP_ARN" grafana_target_group_arn: "$GRAFANA_TARGET_GROUP_ARN" + grafana_sso_secret_arn: "$GRAFANA_SSO_SECRET_ARN" aws_account_id: "$AWS_ACCOUNT_ID" management_clusters: "$MANAGEMENT_CLUSTERS" rhobs_api_url: "$RHOBS_API_URL" From 94f832884909aaa496972ecca881dedd4ca4016a Mon Sep 17 00:00:00 2001 From: Kirk Bater Date: Thu, 25 Jun 2026 14:35:29 -0400 Subject: [PATCH 4/6] explicitly disable non-sso registration --- argocd/config/regional-cluster/grafana/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/argocd/config/regional-cluster/grafana/values.yaml b/argocd/config/regional-cluster/grafana/values.yaml index 5265d6e30..013175a94 100644 --- a/argocd/config/regional-cluster/grafana/values.yaml +++ b/argocd/config/regional-cluster/grafana/values.yaml @@ -78,7 +78,7 @@ grafana: auto_login: true users: - allow_sign_up: true + allow_sign_up: false auto_assign_org_role: Admin analytics: From cfdc2513a9cab90e3f7ac6c50cb6787d08adfa20 Mon Sep 17 00:00:00 2001 From: Kirk Bater Date: Thu, 25 Jun 2026 14:54:14 -0400 Subject: [PATCH 5/6] abstract configmap parsing instead of complicating it --- .../grafana/templates/auth-anonymous.yaml | 42 ++++++++++++++ .../grafana/templates/auth-sso.yaml | 57 +++++++++++++++++++ .../templates/sso-external-secret.yaml | 14 ++++- .../regional-cluster/grafana/values.yaml | 56 ++---------------- 4 files changed, 114 insertions(+), 55 deletions(-) create mode 100644 argocd/config/regional-cluster/grafana/templates/auth-anonymous.yaml create mode 100644 argocd/config/regional-cluster/grafana/templates/auth-sso.yaml diff --git a/argocd/config/regional-cluster/grafana/templates/auth-anonymous.yaml b/argocd/config/regional-cluster/grafana/templates/auth-anonymous.yaml new file mode 100644 index 000000000..0ab15985e --- /dev/null +++ b/argocd/config/regional-cluster/grafana/templates/auth-anonymous.yaml @@ -0,0 +1,42 @@ +{{- if not .Values.grafana.sso.secretArn }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: grafana + namespace: {{ .Release.Namespace }} +data: + grafana.ini: | + [server] + root_url = %(protocol)s://%(domain)s:%(http_port)s/ + + [security] + disable_gravatar = true + + [database] + type = sqlite3 + path = /var/lib/grafana/grafana.db + wal = true + + [auth] + disable_login_form = true + + [auth.anonymous] + enabled = true + org_role = Admin + + [auth.basic] + enabled = false + + [users] + allow_sign_up = false + auto_assign_org_role = Admin + + [analytics] + reporting_enabled = false + check_for_updates = false + check_for_plugin_updates = false + + [log] + mode = console + level = info +{{- end }} diff --git a/argocd/config/regional-cluster/grafana/templates/auth-sso.yaml b/argocd/config/regional-cluster/grafana/templates/auth-sso.yaml new file mode 100644 index 000000000..99e55df13 --- /dev/null +++ b/argocd/config/regional-cluster/grafana/templates/auth-sso.yaml @@ -0,0 +1,57 @@ +{{- if .Values.grafana.sso.secretArn }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: grafana + namespace: {{ .Release.Namespace }} +data: + grafana.ini: | + [server] + root_url = https://%(domain)s/ + + [security] + disable_gravatar = true + + [database] + type = sqlite3 + path = /var/lib/grafana/grafana.db + wal = true + + [auth] + disable_login_form = false + + [auth.anonymous] + enabled = false + + [auth.basic] + enabled = false + + [auth.generic_oauth] + enabled = true + name = Red Hat SSO + allow_sign_up = true + client_id = $__env{SSO_CLIENT_ID} + client_secret = $__env{SSO_CLIENT_SECRET} + scopes = openid email profile offline_access + email_attribute_path = email + login_attribute_path = preferred_username + name_attribute_path = name + auth_url = $__env{SSO_AUTH_URL} + token_url = $__env{SSO_TOKEN_URL} + api_url = $__env{SSO_API_URL} + skip_org_role_sync = true + auto_login = true + + [users] + allow_sign_up = false + auto_assign_org_role = Admin + + [analytics] + reporting_enabled = false + check_for_updates = false + check_for_plugin_updates = false + + [log] + mode = console + level = info +{{- end }} diff --git a/argocd/config/regional-cluster/grafana/templates/sso-external-secret.yaml b/argocd/config/regional-cluster/grafana/templates/sso-external-secret.yaml index 0c552f03e..66573ba3e 100644 --- a/argocd/config/regional-cluster/grafana/templates/sso-external-secret.yaml +++ b/argocd/config/regional-cluster/grafana/templates/sso-external-secret.yaml @@ -12,16 +12,24 @@ spec: target: name: grafana-sso creationPolicy: Owner + template: + engineVersion: v2 + data: + SSO_CLIENT_ID: "{{ "{{ .client_id }}" }}" + SSO_CLIENT_SECRET: "{{ "{{ .client_secret }}" }}" + SSO_AUTH_URL: "{{ "{{ .oidc_url }}" }}/protocol/openid-connect/auth" + SSO_TOKEN_URL: "{{ "{{ .oidc_url }}" }}/protocol/openid-connect/token" + SSO_API_URL: "{{ "{{ .oidc_url }}" }}/protocol/openid-connect/userinfo" data: - - secretKey: client-id + - secretKey: client_id remoteRef: key: {{ .Values.grafana.sso.secretArn }} property: client_id - - secretKey: client-secret + - secretKey: client_secret remoteRef: key: {{ .Values.grafana.sso.secretArn }} property: client_secret - - secretKey: oidc-url + - secretKey: oidc_url remoteRef: key: {{ .Values.grafana.sso.secretArn }} property: oidc_url diff --git a/argocd/config/regional-cluster/grafana/values.yaml b/argocd/config/regional-cluster/grafana/values.yaml index 013175a94..c207050dc 100644 --- a/argocd/config/regional-cluster/grafana/values.yaml +++ b/argocd/config/regional-cluster/grafana/values.yaml @@ -36,59 +36,14 @@ grafana: create: true name: grafana + createConfigmap: false + envFromSecrets: - name: grafana-sso optional: true - "grafana.ini": - server: - root_url: "https://%(domain)s/" - - security: - disable_gravatar: true - - database: - type: sqlite3 - path: /var/lib/grafana/grafana.db - wal: true - - auth: - disable_login_form: false - - "auth.anonymous": - enabled: false - - "auth.basic": - enabled: false - - "auth.generic_oauth": - enabled: true - name: Red Hat SSO - allow_sign_up: true - client_id: $__env{client-id} - client_secret: $__env{client-secret} - scopes: openid email profile offline_access - email_attribute_path: email - login_attribute_path: preferred_username - name_attribute_path: name - auth_url: $__env{oidc-url}/protocol/openid-connect/auth - token_url: $__env{oidc-url}/protocol/openid-connect/token - api_url: $__env{oidc-url}/protocol/openid-connect/userinfo - skip_org_role_sync: true - auto_login: true - - users: - allow_sign_up: false - auto_assign_org_role: Admin - - analytics: - reporting_enabled: false - check_for_updates: false - check_for_plugin_updates: false - - log: - mode: console - level: info + envFromConfigMaps: + - name: grafana-region-env service: type: ClusterIP @@ -98,9 +53,6 @@ grafana: persistence: enabled: false - envFromConfigMaps: - - name: grafana-region-env - datasources: datasources.yaml: apiVersion: 1 From 2b6af78161906ebd8bf3305d7cb730925704b3bc Mon Sep 17 00:00:00 2001 From: Kirk Bater Date: Thu, 25 Jun 2026 15:15:07 -0400 Subject: [PATCH 6/6] adds additional docs on setting up SSO for a new env --- docs/environment-provisioning.md | 58 ++++++++++++++++++++++++++++++-- 1 file changed, 56 insertions(+), 2 deletions(-) diff --git a/docs/environment-provisioning.md b/docs/environment-provisioning.md index 30510f187..11c837ee3 100644 --- a/docs/environment-provisioning.md +++ b/docs/environment-provisioning.md @@ -46,6 +46,16 @@ aws iam update-assume-role-policy \ # Repeat for the Management account (switch credentials first) ``` +### 1.3 Request SSO client registration + +Open a request with Red Hat IT to register a new OIDC client for Grafana in Red Hat SSO. You will need: + +- **Redirect URI**: `https://grafana../login/generic_oauth` (one per region) +- **Grant type**: Authorization Code +- **Scopes**: `openid`, `email`, `profile`, `offline_access` + +Red Hat IT will provide a `client_id`, `client_secret`, and the OIDC realm URL. These are stored in the central account in [step 2.3](#23-store-sso-credentials-in-the-central-account). + ## 2. Configure the Region > **Skip this step** if reusing an existing environment/region configuration. @@ -80,7 +90,51 @@ aws ssm put-parameter --name "/infra/region-ou-path" \ --value "o-xxxxx/r-xxxx/ou-xxxx-xxxxxxxx/ou-xxxx-xxxxxxxx/" --type SecureString ``` -### 2.3 Add the environment configuration +### 2.3 Store SSO credentials in the central account + +Create a Secrets Manager secret in the central account containing the Red Hat SSO credentials from [step 1.3](#13-request-sso-client-registration). The secret must be a JSON object with the following keys: + +```bash +export AWS_PROFILE= + +aws secretsmanager create-secret \ + --name "sso/rh-internal" \ + --secret-string '{ + "client_id": "", + "client_secret": "", + "oidc_url": "" + }' +``` + +Then attach a resource policy that allows the External Secrets Operator role in any regional account (scoped by OU path) to read the secret: + +```bash +aws secretsmanager put-resource-policy \ + --secret-id "sso/rh-internal" \ + --resource-policy '{ + "Version": "2012-10-17", + "Statement": [{ + "Effect": "Allow", + "Principal": "*", + "Action": ["secretsmanager:GetSecretValue", "secretsmanager:DescribeSecret"], + "Resource": "*", + "Condition": { + "StringLike": { + "aws:PrincipalArn": "arn:aws:iam::*:role/*-external-secrets-operator" + }, + "ForAnyValue:StringLike": { + "aws:PrincipalOrgPaths": "/*" + } + } + }] + }' +``` + +Replace `` with the OU path for regional accounts (e.g. `o-xxxxx/r-xxxx/ou-xxxx-xxxxxxxx`). New regions automatically get access without updating this policy. + +> **Note:** This step is only required once per environment. The identity policy on the ESO role (granting cross-account access to this secret) is provisioned automatically by Terraform when `enable_grafana_ingress` is enabled. + +### 2.4 Add the environment configuration Create a new region config file at `config//.yaml`. This inherits defaults from `config/defaults.yaml` — override only what differs. Environment-level defaults can be set in `config//defaults.yaml`. @@ -100,7 +154,7 @@ management_clusters: mc01: {} ``` -### 2.4 Render and commit +### 2.5 Render and commit ```bash uv run scripts/render.py