# kubera read-only governance role — CloudFormation template, policy version v1.
#
# What this grants (and what it cannot do) is documented statement-by-statement
# in docs/aws-onboarding-policy.md. The inline policy below is the SINGLE source
# of runtime permission: no managed policies, no wildcards on actions, and the
# only iam:* access beyond trust-metadata reads is the role reading ITS OWN
# definition so kubera can verify the deployed policy still matches this
# published document (drift check).
#
# There is deliberately ONE policy — this one. It carries the full read-only
# permission surface for fleet discovery, cluster-related resource discovery,
# and change tracking. Pre-GA (zero deployed customers) the v1 policy grew in
# place instead of forking a v2 + upgrade flow; if kubera flags your deployed
# role as missing actions, re-run the same deploy command to update the stack.
#
# Long-form intrinsics (Ref:/Fn::Sub:/Fn::GetAtt:) are used instead of the !Ref
# shorthand tags so the template stays parseable by any plain YAML parser — a
# test yaml.Unmarshals this file and asserts the action list against the same
# expected-actions table the drift checker uses.
#
# This file is embedded into the kubera control plane and served at
# GET /v1/aws/onboarding/template.yaml; deploy/aws/onboarding/kubera-role.yaml
# is the published copy and must stay byte-identical (a test enforces it).
AWSTemplateFormatVersion: '2010-09-09'
Description: >-
  kubera governance read-only role (policy v1). Grants the kubera control plane
  read-only discovery of EKS clusters, their cluster-related AWS resources
  (K8s-born load balancers/volumes/autoscaling groups, the cluster's security
  groups/subnets/VPC, IRSA role trust metadata, EKS log groups), the
  dependency-fabric surface (data-service/ingress/network-path/DNS/certificate/
  registry metadata), CloudTrail event history plus service state events
  (RDS/ElastiCache/scaling/Health), diagnosis-time CloudWatch readbacks, and
  AMP scrapers in this account. It cannot modify anything, cannot read
  Kubernetes Secrets, secret VALUES, or workload data, and beyond IAM trust
  METADATA it can read exactly one IAM role definition in depth: itself.

Parameters:
  KuberaAccountId:
    Type: String
    Description: The kubera control plane's AWS account id (the trusted principal).
    AllowedPattern: '[0-9]{12}'
    ConstraintDescription: must be a 12-digit AWS account id
  ExternalId:
    Type: String
    Description: >-
      The per-connection ExternalId minted by kubera (shown on the Connect AWS
      screen). Confused-deputy protection - do not change it.
    AllowedPattern: '[a-f0-9]{32}'
    ConstraintDescription: must be the 32-hex-character id minted by kubera
  RoleName:
    Type: String
    Default: KuberaGovernanceReadOnly
    Description: Name of the IAM role to create.

Resources:
  KuberaGovernanceRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName:
        Ref: RoleName
      Description: >-
        kubera governance platform - read-only EKS fleet discovery. Assumable
        only by the kubera account presenting the ExternalId; every session is
        tagged kubera_tenant/kubera_purpose so this account's CloudTrail
        attributes each call.
      MaxSessionDuration: 3600
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          # Assume: only the kubera account, only with the minted ExternalId
          # (confused-deputy prevention).
          - Sid: KuberaAssume
            Effect: Allow
            Principal:
              AWS:
                Fn::Sub: 'arn:aws:iam::${KuberaAccountId}:root'
            Action: 'sts:AssumeRole'
            Condition:
              StringEquals:
                'sts:ExternalId':
                  Ref: ExternalId
          # Session tags: kubera stamps kubera_tenant=<org> /
          # kubera_purpose=<probe|sweep|...> so THIS account's CloudTrail
          # attributes every kubera call to a tenant and a job.
          #
          # sts:TagSession MUST be its own statement and MUST NOT be gated on
          # sts:ExternalId: STS does not place the ExternalId in the request
          # context when it authorizes the TagSession action, so an
          # ExternalId condition on TagSession can never be satisfied and
          # silently denies all tagging (the earlier single-statement form had
          # exactly this bug). aws:TagKeys IS valid for TagSession and pins
          # kubera to exactly its two attribution keys. TagSession alone
          # assumes nothing, so the ExternalId gate on KuberaAssume above still
          # fully protects the role.
          - Sid: KuberaTagSession
            Effect: Allow
            Principal:
              AWS:
                Fn::Sub: 'arn:aws:iam::${KuberaAccountId}:root'
            Action: 'sts:TagSession'
            Condition:
              'ForAllValues:StringEquals':
                'aws:TagKeys':
                  - kubera_tenant
                  - kubera_purpose
      Policies:
        # ONE inline policy, versioned v1. The action list mirrors
        # docs/aws-onboarding-policy.md and pkg/awsonboard.ExpectedActions
        # exactly; kubera's drift check compares the deployed set against that
        # table, flags any widening on the dashboard, and reads missing
        # actions as "deployed from an older template - re-run the deploy".
        - PolicyName: kubera-governance-readonly-v1
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              # ListPodIdentityAssociations/DescribePodIdentityAssociation read
              # the cluster's EKS Pod Identity bindings - which IAM role a
              # namespace/service-account pair is bound to. Metadata only;
              # kubera cannot create, change, or assume any binding or role.
              - Sid: EKSDiscoveryRead
                Effect: Allow
                Action:
                  - 'eks:ListClusters'
                  - 'eks:DescribeCluster'
                  - 'eks:ListNodegroups'
                  - 'eks:DescribeNodegroup'
                  - 'eks:ListAddons'
                  - 'eks:DescribeAddon'
                  - 'eks:ListFargateProfiles'
                  - 'eks:DescribeFargateProfile'
                  - 'eks:ListPodIdentityAssociations'
                  - 'eks:DescribePodIdentityAssociation'
                Resource: '*'
              - Sid: EC2KubernetesBornRead
                Effect: Allow
                Action:
                  - 'ec2:DescribeVolumes'
                  - 'ec2:DescribeInstances'
                  - 'ec2:DescribeRegions'
                Resource: '*'
              # The cluster's OWN network context: security groups, subnets,
              # and VPC are looked up by the exact ids eks:DescribeCluster
              # returns - names, rule counts, and CIDRs only. Metadata reads;
              # kubera cannot change a rule or see traffic.
              - Sid: NetworkContextRead
                Effect: Allow
                Action:
                  - 'ec2:DescribeSecurityGroups'
                  - 'ec2:DescribeSubnets'
                  - 'ec2:DescribeVpcs'
                Resource: '*'
              # Kubernetes-born load balancers: DescribeTags is what proves a
              # load balancer belongs to a cluster (kubernetes.io/cluster/<name>,
              # elbv2.k8s.aws/cluster, kubernetes.io/service-name tags).
              # Untagged load balancers are ignored - this is not an ELB inventory.
              - Sid: ELBKubernetesBornRead
                Effect: Allow
                Action:
                  - 'elasticloadbalancing:DescribeLoadBalancers'
                  - 'elasticloadbalancing:DescribeTags'
                Resource: '*'
              # Kubernetes-born autoscaling groups, attributed by their
              # eks:cluster-name / kubernetes.io/cluster/<name> tags.
              - Sid: ASGKubernetesBornRead
                Effect: Allow
                Action:
                  - 'autoscaling:DescribeAutoScalingGroups'
                Resource: '*'
              # IRSA trust reads: list+get OIDC providers and role trust
              # documents to find the IAM roles trusting a cluster's OIDC
              # issuer (IAM Roles for Service Accounts). This reads role
              # METADATA only - names, ARNs, and trust policies - never the
              # roles' permission policies, and grants no ability to act as,
              # modify, or create any role.
              - Sid: IRSATrustRead
                Effect: Allow
                Action:
                  - 'iam:ListOpenIDConnectProviders'
                  - 'iam:GetOpenIDConnectProvider'
                  - 'iam:ListRoles'
                Resource: '*'
              # EKS control-plane log groups (/aws/eks/<cluster>/cluster) -
              # a prefix query for names/retention. Cannot read log events.
              - Sid: EKSLogGroupRead
                Effect: Allow
                Action:
                  - 'logs:DescribeLogGroups'
                Resource: '*'
              # Change tracking: LookupEvents is the read-only 90-day
              # CloudTrail event history (who changed what, when).
              # cloudformation:GetResource is the IAM action behind the AWS
              # Cloud Control API's read-only GetResource (Cloud Control's
              # permissions live in the cloudformation namespace) — it reads
              # one resource's current state for richer before/after diffs.
              # Neither can modify anything; the Cloud Control create/update/
              # delete verbs are not granted.
              - Sid: ChangeTrackingRead
                Effect: Allow
                Action:
                  - 'cloudtrail:LookupEvents'
                  - 'cloudformation:GetResource'
                Resource: '*'
              - Sid: AMPTelemetryBindingRead
                Effect: Allow
                Action:
                  - 'aps:ListScrapers'
                  - 'aps:DescribeScraper'
                  - 'aps:ListWorkspaces'
                Resource: '*'
              # Data-service inventory: the databases/caches/brokers/queues/
              # tables that workload dependencies resolve to. Metadata only
              # (endpoints, engine versions, parameter-group names) - no data
              # access: kubera cannot query a database, read a queue message,
              # consume a topic, or read a table item. A resource still earns
              # a kubera record ONLY through a cluster/workload relationship.
              - Sid: DataServiceInventoryRead
                Effect: Allow
                Action:
                  - 'rds:DescribeDBInstances'
                  - 'rds:DescribeDBClusters'
                  - 'elasticache:DescribeCacheClusters'
                  - 'elasticache:DescribeReplicationGroups'
                  - 'kafka:ListClusters'
                  - 'kafka:ListClustersV2'
                  - 'kafka:DescribeCluster'
                  - 'kafka:DescribeClusterV2'
                  - 'sqs:ListQueues'
                  - 'sqs:GetQueueAttributes'
                  - 'dynamodb:ListTables'
                  - 'dynamodb:DescribeTable'
                Resource: '*'
              # Ingress-path chain: target groups, listeners, and target
              # HEALTH STATE (which backend a load balancer considers dead -
              # the mass-deregistration incident signal), plus API Gateway
              # read-only (apigateway:GET is the read verb for its REST
              # surface: APIs, stages, usage plans). Cannot modify any of it.
              - Sid: IngressPathRead
                Effect: Allow
                Action:
                  - 'elasticloadbalancing:DescribeTargetGroups'
                  - 'elasticloadbalancing:DescribeListeners'
                  - 'elasticloadbalancing:DescribeTargetHealth'
                  - 'apigateway:GET'
                Resource: '*'
              # Network-path context beyond the cluster's own SG/subnet/VPC:
              # route tables, NAT gateways, VPC endpoints, NACLs, and the
              # transit-gateway attachments that stitch VPCs together - the
              # fabric through which cluster traffic reaches its dependencies.
              # Metadata only; cannot change a route or see traffic.
              - Sid: NetworkPathRead
                Effect: Allow
                Action:
                  - 'ec2:DescribeRouteTables'
                  - 'ec2:DescribeNatGateways'
                  - 'ec2:DescribeVpcEndpoints'
                  - 'ec2:DescribeNetworkAcls'
                  - 'ec2:DescribeTransitGatewayVpcAttachments'
                Resource: '*'
              # DNS zones + records (external-dns ownership and record-fight
              # detection) and certificate inventory with expiry horizons.
              # Cannot change a record or request/renew/delete a certificate.
              - Sid: DNSAndCertRead
                Effect: Allow
                Action:
                  - 'route53:ListHostedZones'
                  - 'route53:ListResourceRecordSets'
                  - 'acm:ListCertificates'
                  - 'acm:DescribeCertificate'
                Resource: '*'
              # Container-image metadata for running-image edges: repository
              # names, image digests/tags, push times. Cannot pull an image,
              # read a layer, or modify a repository.
              - Sid: RegistryImageRead
                Effect: Allow
                Action:
                  - 'ecr:DescribeRepositories'
                  - 'ecr:DescribeImages'
                Resource: '*'
              # State-transition events - the "nothing changed" causes that
              # CloudTrail cannot see: RDS failovers/maintenance/low-storage,
              # ElastiCache failovers/evictions, scaling activities (spot
              # reclaims, InsufficientInstanceCapacity), AWS Health (zonal
              # impairment). rds:DescribeDBParameters enriches parameter-group
              # events with the applied values + apply method (the
              # pending-reboot diagnosis) - configuration metadata, never data.
              # NOTE: the health:* reads require the account to
              # have a Business/Enterprise support plan; where denied, kubera
              # degrades gracefully and infers from per-AZ readbacks instead.
              - Sid: StateEventRead
                Effect: Allow
                Action:
                  - 'rds:DescribeEvents'
                  - 'rds:DescribeDBParameters'
                  - 'elasticache:DescribeEvents'
                  - 'autoscaling:DescribeScalingActivities'
                  - 'health:DescribeEvents'
                  - 'health:DescribeEventDetails'
                  - 'health:DescribeAffectedEntities'
                Resource: '*'
              # Diagnosis-time readbacks: CloudWatch vended metrics (database
              # connections, ELB 5xx splits, NAT port-allocation errors,
              # burst balance) queried at incident-diagnosis time, and RDS
              # Performance Insights metrics. Reads numbers kubera never
              # stores as timeseries; cannot write or delete any metric.
              - Sid: DiagnosisReadbackRead
                Effect: Allow
                Action:
                  - 'cloudwatch:GetMetricData'
                  - 'cloudwatch:ListMetrics'
                  - 'pi:GetResourceMetrics'
                  - 'pi:DescribeDimensionKeys'
                  - 'rds-db:connect'
                Resource: '*'
              # Secret METADATA only: names, rotation configuration, and
              # last-rotated timestamps - the "credentials rotated at T"
              # change signal. secretsmanager:GetSecretValue is deliberately
              # NOT granted: kubera can never read a secret's value.
              - Sid: SecretsMetadataRead
                Effect: Allow
                Action:
                  - 'secretsmanager:ListSecrets'
                  - 'secretsmanager:DescribeSecret'
                Resource: '*'
              # Agentless posture sweep: the full-account, read-only config
              # reads behind kubera's CIS-cited AWS findings (security-group
              # exposure, S3 public-access/versioning, IAM credential hygiene,
              # RDS/DynamoDB/ElastiCache resilience flags, WAF coverage).
              # Metadata and configuration only — no data-plane reads, and
              # Lambda environment-variable VALUES never leave the account
              # (kubera records variable names only).
              - Sid: AgentlessPostureRead
                Effect: Allow
                Action:
                  - 'ec2:DescribeFlowLogs'
                  - 'elasticloadbalancing:DescribeLoadBalancerAttributes'
                  - 'lambda:ListFunctions'
                  - 'lambda:GetFunctionConcurrency'
                  - 'dynamodb:DescribeContinuousBackups'
                  - 'cloudwatch:DescribeAlarms'
                  - 'tag:GetResources'
                  - 'wafv2:ListWebACLs'
                  - 'wafv2:ListResourcesForWebACL'
                  - 'iam:GenerateCredentialReport'
                  - 'iam:GetCredentialReport'
                  - 'iam:ListPolicies'
                  - 'iam:GetPolicyVersion'
                  - 's3:ListAllMyBuckets'
                  - 's3:GetBucketPublicAccessBlock'
                  - 's3:GetBucketVersioning'
                Resource: '*'
              # Self-inspection ONLY: scoped to this role's own ARN so kubera
              # can verify the deployed policy still matches the published v1
              # document. It cannot read any other role's inline policies.
              - Sid: SelfPolicyDriftRead
                Effect: Allow
                Action:
                  - 'iam:GetRole'
                  - 'iam:ListRolePolicies'
                  - 'iam:GetRolePolicy'
                Resource:
                  Fn::Sub: 'arn:aws:iam::${AWS::AccountId}:role/${RoleName}'

Outputs:
  RoleArn:
    Description: Paste this Role ARN into kubera's Connect AWS screen.
    Value:
      Fn::GetAtt: [KuberaGovernanceRole, Arn]
