Ir al contenido

Alcances y permisos

Credenciales por connector te dice qué claves crear. Esta página te dice con qué permisos crearlas.

Dos credenciales de naturaleza distinta: la API key no tiene alcances, la Application key sí.

Alcance Habilita ¿Obligatorio?
ninguno metrics.* Datadog no expone un alcance de lectura de métricas: una app key estándar ya lee métricas.
logs_read_data + logs_read_index_data logs.* Si usas logs
apm_read traces.*, topology.* (Software Catalog) Si usas APM
monitors_read monitors.* Siempre (ver abajo)
incident_read incidentes Si usas incidentes
rum_apps_read rum.* Si usas RUM
synthetics_read synthetics.* Si Datadog sirve synthetics
api_keys_read solo la higiene de claves (security) Opcional, y sensible

Sin alcances. Solo se usa en /api/v1/validate, en el health check.

No es un permiso, pero falla junto con uno: si tu organización es EU o US3/US5/AP1, una key válida devuelve 403 contra el site equivocado. Revisa esto antes de investigar alcances.

Alcances de Access Policy. Solo los dos primeros son obligatorios.

Alcance Habilita ¿Obligatorio?
metrics:read Mimir: metrics.*, service graph, métricas de Faro y Synthetics Sí
logs:read Loki: logs.* No; se degrada por operación
traces:read Tempo: traces.* No; se degrada por operación
alerts:read + rules:read Alerting: monitors.* No
stacks:read + stack-dashboards:read búsqueda de dashboards en el stack No
Synthetic Monitoring synthetics.* vía /api/v1/check/list Nombre del alcance por confirmar en tu cuenta

Mimir es el único obligatorio porque es la columna vertebral: sin él el connector no tiene nada que servir. Los demás se degradan con un error accionable, no con una falla de boot.

Una policy de IAM de solo lectura, servida como archivo para que no tengas que copiarla de la pantalla:

Ventana de terminal
curl -O https://docs.whystack.ai/aws/whystack-edge-readonly.json
aws iam create-policy \
--policy-name WhyStackEdgeReadOnly \
--policy-document file://whystack-edge-readonly.json

Cubre todo el catálogo. Si no usas una capability, quita el Statement correspondiente antes de crearla. Para eso están los Sid: cada uno es un dominio removible.

El archivo es el canónico; el bloque de abajo es el mismo contenido, y una verificación del build falla si los dos se separan.

{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "IdentidadeEContexto",
"Effect": "Allow",
"Action": [
"sts:GetCallerIdentity",
"organizations:DescribeOrganization",
"organizations:ListAccounts"
],
"Resource": "*"
},
{
"Sid": "CustoEFinOps",
"Effect": "Allow",
"Action": [
"ce:GetCostAndUsage",
"ce:GetAnomalies",
"ce:GetReservationCoverage",
"ce:GetReservationPurchaseRecommendation",
"ce:GetSavingsPlansCoverage",
"ce:GetSavingsPlansPurchaseRecommendation",
"compute-optimizer:GetEnrollmentStatus",
"compute-optimizer:GetEC2InstanceRecommendations",
"compute-optimizer:GetEBSVolumeRecommendations",
"compute-optimizer:GetLambdaFunctionRecommendations",
"compute-optimizer:GetAutoScalingGroupRecommendations",
"compute-optimizer:GetRDSDatabaseRecommendations",
"pricing:GetProducts"
],
"Resource": "*"
},
{
"Sid": "Compute",
"Effect": "Allow",
"Action": [
"ec2:DescribeInstances",
"ec2:DescribeAddresses",
"ec2:DescribeNetworkInterfaces",
"ec2:DescribeSecurityGroups",
"ec2:DescribeSubnets",
"ec2:DescribeVpcs",
"ec2:DescribeVpcEndpoints",
"ec2:DescribeVpcPeeringConnections",
"ec2:DescribeRouteTables",
"ec2:DescribeInternetGateways",
"ec2:DescribeNatGateways",
"ec2:DescribeCustomerGateways",
"ec2:DescribeVpnConnections",
"ec2:DescribeVpnGateways",
"ec2:DescribeTransitGateways",
"ec2:DescribeTransitGatewayAttachments",
"ec2:DescribeManagedPrefixLists",
"ec2:DescribeVolumes",
"ec2:DescribeSnapshots",
"ec2:DescribeImages",
"autoscaling:DescribeAutoScalingGroups",
"ecs:ListClusters",
"eks:ListClusters",
"eks:DescribeCluster",
"eks:ListNodegroups",
"eks:DescribeNodegroup",
"ecr:DescribeRepositories",
"ecr:DescribeImages",
"lambda:ListFunctions",
"lambda:GetFunctionConfiguration",
"lambda:ListEventSourceMappings"
],
"Resource": "*"
},
{
"Sid": "BancosDeDados",
"Effect": "Allow",
"Action": [
"rds:DescribeDBInstances",
"rds:DescribeDBClusters",
"rds:DescribeDBSubnetGroups",
"rds:ListTagsForResource",
"dms:DescribeReplicationInstances",
"dms:DescribeReplicationTasks",
"es:ListDomainNames",
"es:DescribeDomains",
"elasticache:DescribeCacheClusters",
"elasticache:DescribeReplicationGroups",
"pi:DescribeDimensionKeys",
"pi:GetResourceMetrics"
],
"Resource": "*"
},
{
"Sid": "Rede",
"Effect": "Allow",
"Action": [
"elasticloadbalancing:DescribeLoadBalancers",
"elasticloadbalancing:DescribeTargetGroups",
"elasticloadbalancing:DescribeTargetHealth",
"elasticloadbalancing:DescribeInstanceHealth",
"elasticloadbalancing:DescribeTags",
"route53:ListHostedZones",
"route53:GetHostedZone",
"route53:ListResourceRecordSets",
"sqs:ListQueues",
"sqs:GetQueueUrl",
"sqs:GetQueueAttributes"
],
"Resource": "*"
},
{
"Sid": "Observabilidade",
"Effect": "Allow",
"Action": [
"cloudwatch:GetMetricData",
"cloudwatch:DescribeAlarms",
"logs:DescribeLogGroups",
"logs:FilterLogEvents",
"logs:StartQuery",
"logs:GetQueryResults",
"application-signals:ListServices",
"application-signals:ListServiceLevelObjectives",
"application-signals:BatchGetServiceLevelObjectiveBudgetReport"
],
"Resource": "*"
},
{
"Sid": "AuditoriaEConsulta",
"Effect": "Allow",
"Action": [
"cloudtrail:LookupEvents",
"cloudtrail:ListEventDataStores",
"cloudtrail:StartQuery",
"cloudtrail:DescribeQuery",
"cloudtrail:GetQueryResults",
"athena:StartQueryExecution",
"athena:StopQueryExecution",
"athena:GetQueryExecution",
"athena:BatchGetQueryExecution",
"athena:GetQueryResults",
"athena:ListQueryExecutions"
],
"Resource": "*"
},
{
"Sid": "SegurancaIamSomenteLeitura",
"Effect": "Allow",
"Action": ["iam:ListUsers", "iam:ListAccessKeys", "iam:GetAccessKeyLastUsed"],
"Resource": "*"
},
{
"Sid": "BiQuickSightSomenteLeitura",
"Effect": "Allow",
"Action": ["quicksight:Describe*", "quicksight:List*", "quicksight:Search*"],
"Resource": "*"
}
]
}

1. athena:StartQueryExecution y cloudtrail:StartQuery parecen escrituras, y no lo son. Así es como se ejecuta una consulta de lectura en esos servicios: la acción crea una ejecución, no modifica datos. El handler valida el SQL primero: DDL y DML se rechazan. Si tu revisión de seguridad bloquea Start* por reflejo, ese es el argumento.

2. Athena necesita S3, y no está arriba. Los resultados van a un bucket, y sin s3:GetObject/s3:PutObject/s3:ListBucket sobre el bucket de resultados, la consulta falla. Según tu catálogo, también glue:GetTable/GetDatabase/GetPartitions. Quedó fuera a propósito: el ARN es tuyo, y un comodín aquí sería peor que una omisión explícita.

3. DocumentDB usa el namespace rds:. No existe docdb:, y buscarlo sin encontrarlo lleva a conceder de más.

4. Tres prefijos no coinciden con el nombre del servicio: CloudWatch Logs es logs:, Cost Explorer es ce:, OpenSearch es es:.

5. Sin embed de QuickSight. El connector tiene código para GenerateEmbedUrlFor* y GetDashboardEmbedUrl, pero esas cuatro operaciones se difirieron por postura y no están en el catálogo: el enforcement las rechaza antes de que lleguen al connector. Conceder esos permisos sería un privilegio que nada puede ejercer. La lectura para bi es el statement BiQuickSightSomenteLeitura (quicksight:Describe*, List* y Search*, que es lo que las operaciones del catálogo realmente llaman), y queda fuera solo si lo quitas. Hasta septiembre de 2026 ese statement no estaba en la policy servida (la página decía que había que añadirlo a mano): si aplicaste la policy antes de esa fecha, toda lectura de QuickSight recibe AccessDenied, y la solución es añadir el statement, no rotar la credencial.

Dos credenciales de AWS distintas: no las confundas

Sección titulada «Dos credenciales de AWS distintas: no las confundas»
Para qué Cómo Permiso
Leer tus fuentes RUNNER_AWS_CRED_MODE: static (claves) o chain (IRSA, instance profile) La policy de arriba
Leer tus secretos RUNNER_SECRET_STORE_MODE=aws secretsmanager:GetSecretValue, acotado al prefijo de tus secretos

La segunda debería restringirse al ARN, no a *:

{
"Effect": "Allow",
"Action": "secretsmanager:GetSecretValue",
"Resource": "arn:aws:secretsmanager:REGION:ACCOUNT:secret:whystack/*"
}

Los sso:GetRoleCredentials y sso:ListAccountRoles que vas a encontrar en el código pertenecen al camino de desarrollo con aws sso login. No los concedas en producción.

Aquí los permisos vienen en dos capas, y conceder solo la primera es el error más común.

Capa 1, IAM: eks:DescribeCluster (ya está en la policy de arriba), para descubrir el endpoint y la CA del clúster.

Capa 2, RBAC dentro del clúster. El connector genera un token como lo hace aws eks get-token y habla con el API server. Una identidad IAM sin binding en el clúster recibe 401/403 de Kubernetes, no de AWS, y el error no menciona IAM, lo que manda la investigación en la dirección equivocada.

El connector lee solo esto:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: whystack-read
rules:
- apiGroups: [""]
resources: ["pods", "services", "endpoints"]
verbs: ["get", "list"]
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get"]
- apiGroups: ["networking.k8s.io"]
resources: ["ingresses"]
verbs: ["get", "list"]

Vincúlalo a la identidad IAM del runner con una EKS access entry (o una entrada en aws-auth en clústeres más antiguos) más un ClusterRoleBinding.

Sin EKS_CLUSTER en el entorno, el health check responde skipped: no cluster configured, que no es un error de permisos, pero lo parece.

Roles a nivel de proyecto. roles/viewer cubre la mayor parte; estos son los que suelen faltar:

Rol Habilita
roles/viewer Compute, GKE, Cloud SQL, Run, Functions, DNS, Artifact Registry, Firestore, Redis
roles/monitoring.viewer metrics.*
roles/logging.viewer logs.*
roles/cloudasset.viewer inventario de assets
roles/recommender.viewer recomendaciones
roles/securitycenter.findingsViewer security.*
roles/billing.viewer en la billing account costo. Fíjate que es en la billing account, no en el proyecto
roles/bigquery.dataViewer + roles/bigquery.jobUser leer GCP_BILLING_EXPORT_TABLE
roles/iam.securityReviewer lecturas de IAM

El costo es el más delicado: necesita el rol en la billing account y acceso de BigQuery a la tabla de export. Sin GCP_BILLING_EXPORT_TABLE, la operación falla de forma explícita.

El precio de lista por vCPU y por GiB (cost.getGcpComputePricing, que le pone una cifra en dólares a la holgura de un clúster de GKE) viene del catálogo público de Cloud Billing: no necesita ningún rol, solo la API cloudbilling.googleapis.com habilitada en el proyecto de la service account, la misma que ya usa el listado de billing accounts.

La lectura de la tabla de export corre en tu cuenta, y BigQuery cobra por byte leído. Por eso está limitada de tres formas:

  • Podada por la partición de la tabla. El Agent lee los metadatos de la tabla, que no se cobran, y filtra por su partición: el tiempo de ingesta en el export estándar, o la columna de partición en un export personalizado.
  • Un tope de 50 GB por consulta (maximumBytesBilled), unos US$0.31 con precios on-demand. Por encima de eso, BigQuery rechaza la consulta y la respuesta pide una ventana más estrecha.
  • Costo bruto y neto lado a lado. La columna cost del export es bruta; los descuentos (uso sostenido, compromisos, contrato) vienen en credits. La respuesta trae los dos y el neto, que es lo que pagas.

Con RUNNER_GCP_CRED_MODE=chain, estos roles van en la GSA de Workload Identity y no circula ninguna clave.

Token fine-grained (recomendado):

Permiso Nivel Para qué
Metadata Read Obligatorio. La base de todos los demás
Contents Read Commits, contenido, refs, tags, comparación, búsqueda de código
Pull requests Read PRs, archivos de PR, PRs asociados a un commit
Actions Read Ejecuciones de workflow
Checks Read Checks de un ref
Administration Read Solo la protección de branch (ver abajo)
Dependabot alerts Read Alertas de seguridad
Issues Read and write Opcional. Solo para abrir issues (create_github_issue)

Token clásico, si lo prefieres: repo + read:org + security_events. El alcance repo concede escritura sobre todo el repositorio; el connector no la usa, pero la credencial la permite. Por eso el token fine-grained es el recomendado.

El Agent verifica el tipo de token en cada health check, y la Flota muestra el resultado en el check fine_grained_token. Un token que no es fine-grained sigue funcionando, y el connector muestra una configuración pendiente que nombra el tipo de token y los alcances que concede. El token en sí nunca sale del Agent: el check lleva solo el prefijo (ghp_, github_pat_) y la lista de alcances.

Aparte de Issues, ninguno de estos permisos es de escritura, y el connector tiene una sola llamada que modifica algo: la que abre un issue. Sin Issues: Read and write, esa escritura no ocurre y el resto de la tabla funciona igual. Consulta Modelo de seguridad y Qué escribe el agente.

Alcances del bot token:

Alcance Para qué
channels:read, groups:read listar conversaciones
channels:history, groups:history leer el historial
chat:write chat.postMessage, operación de escritura
channels:manage chat.createChannel, operación de escritura
Plataforma Qué necesita la credencial Notas
Checkly Lectura de checks y resultados (/checks, /check-results/{id}) En la práctica CHECKLY_ACCOUNT_ID es obligatorio para un token de usuario; sin él, un 401 sin explicación
Vercel Lectura de deployments y proyectos (/v6/deployments, /v9/projects) Token de solo lectura acotado al equipo; VERCEL_TEAM_ID es obligatorio para un token de equipo, si no, un 403 silencioso
Argo CD Cuenta local con applications, get y nada más Ver la política abajo. sync, rollback, delete y action no las usa el producto, así que concederlas es un privilegio que nada puede ejercer
Amplitude API key + secret con acceso a las APIs de Dashboard/Export Cubre /events/segmentation, /funnels, /dashboards, /charts. Lecturas
SonarQube Token de usuario con Browse sobre los proyectos analizados /api/issues/search, /api/measures/component, /api/qualitygates/project_status, /api/hotspots/search
Jira Browse projects, más Create issues, Add comments y Transition issues Las únicas tres escrituras de tickets del catálogo. Sin ellas, las lecturas siguen funcionando
Cloudflare Zone:Read + DNS:Read Acótalo a las zonas que necesitas, no a toda la cuenta
Azion Token con lectura del workspace (applications, connectors, workloads) y de las APIs GraphQL de métricas y eventos Los POST del connector son transporte de query GraphQL, no mutations
MongoDB Usuario con read sobre las bases de datos consultadas MONGO_URI incluye la credencial; usa un usuario dedicado de solo lectura

Crea una cuenta local, dale solo lectura de applications y emite el token:

# argocd-cm: enable the account
data:
accounts.whystack: apiKey
# argocd-rbac-cm: what it may do
data:
policy.csv: |
p, role:whystack, applications, get, */*, allow
g, whystack, role:whystack
Ventana de terminal
argocd account generate-token --account whystack

Tres notas que importan más que la política en sí:

  • applications, get alcanza. Las tres operaciones de WhyStack (listar applications, detalle e historial de sync) leen la misma Application; el historial de sync no tiene endpoint propio, es un campo de status. Conceder sync, override o delete no habilita nada: ninguna op del catálogo los usa, y el runner rechaza lo que no está en el catálogo antes incluso de validar los argumentos.
  • Una lectura puede disparar una reconciliación, y no es decisión nuestra. WhyStack nunca envía el parámetro refresh. Pero leer el árbol de recursos pasa por la caché Redis de Argo CD, y en un cache miss el propio servidor dispara un refresh normal (comparar contra git) antes de responder, sin ningún parámetro para desactivarlo. Conviene saberlo al dimensionar: es una comparación con git, no un nuevo render de los manifests.
  • Sin permiso la lista vuelve vacía, y eso no es un error. El servidor de Argo CD filtra los listados por RBAC en silencio: un token sin applications, get recibe una lista vacía con un 200. Para que eso no se lea como “este clúster no tiene applications”, WhyStack le pregunta al propio Argo CD (can-i) cada vez que la lista vuelve vacía, y reporta la diferencia entre no hay y no puedo verlas.

Antes de una auditoría, verifica contra el árbol en lugar de confiar en la página:

Ventana de terminal
# AWS commands actually used, grouped by service
grep -rhoE "\bnew [A-Z][A-Za-z0-9]+Command\b" packages/connectors/src | sort -u
# A connector's REST endpoints
grep -ohE "'/[a-zA-Z0-9/_.{}-]+'" packages/connectors/src/sonarqube.ts | sort -u
# What's actually exposed (enforcement refuses the rest)
grep -oE "'[a-z]+\.[a-zA-Z]+'" apps/runner/src/connectors/manifests/real.ts | sort -u

Esa última línea es la que importa para el mínimo privilegio: un permiso para una operación que no está en el catálogo es un privilegio que nada puede ejercer. Así es como las acciones de embed de QuickSight salieron de esta página.