Onboarding the AWS plugin (keyless, Web Identity Federation)
This guide lets you authorize Linro to scan your AWS account without handing
over any long-lived credential — no access keys, and no IAM user Linro holds.
Linro's managed sensor proves its identity with a short-lived Kubernetes token
and assumes a read-only role you control via
sts:AssumeRoleWithWebIdentity. Nothing long-lived ever exists.
This is the human operator's guide — console click-paths, Terraform, and
troubleshooting. Onboarding from an AI agent instead? Follow
onboarding-agent.md, which restates every step as
structured, agent-executable parameters (and is what Linro's MCP serves to a
driving agent). Both produce the identical AWS trust; pick whichever fits how
you're onboarding.
How it works (30-second version)
managed sensor pod AWS (your account)
────────────────── ───────────────────
projected K8s token ──STS──▶ IAM OIDC identity provider
iss = Linro cluster (trusts Linro's cluster issuer,
aud = linro-managed-sensor client id = linro-managed-sensor)
sub = system:serviceaccount: │
linro-<your-install>:* ▼
AssumeRoleWithWebIdentity → linro-scanner role
(SecurityAudit + ViewOnlyAccess)
│
▼
read-only inventory scan
The trust anchor is your install's namespace (linro-<slug>), carried in
the token's sub claim. Any managed sensor Linro runs in that namespace can
assume the role; no other Linro install — on the same shared cluster, same
issuer, same audience — can, because the role's trust policy pins your
namespace. There is no per-sensor configuration and no secret to rotate.
Step 0 — Get your install's exact values
Three values describe your install. Do not guess or copy them from this
doc — read them from your own install, which is the source of truth:
| Value | What it is | Example |
|---|
| namespace | The Kubernetes namespace your install runs in (the trust anchor) | linro-acme |
| OIDC issuer | Linro's cluster issuer URL (same for every install on a cluster) | https://oidc.eks.eu-west-1.amazonaws.com/id/240C82FA... |
| audience | The token audience / OIDC client id (fixed) | linro-managed-sensor |
Two equivalent ways to read them:
- UI: in your Linro install, open System → Security. The "Cloud
federation" panel shows all three and a pre-filled snippet.
- API: call
SystemService.GetCloudFederationInfo (over MCP, the
GetCloudFederationInfo tool; or grpcurl against your install host). It
returns namespace, oidc_issuer_url, audience, token_path, and
oidc_discovery_url. To confirm the issuer is reachable from AWS before
configuring it, curl the oidc_discovery_url (the issuer's
/.well-known/openid-configuration) and the jwks_uri it points at.
Both are backed by the same API, so they always reflect the live install.
Step 1 — Create the AWS trust (Terraform, recommended)
Use the example module at
deploy/terraform/examples/onboarding-wif/.
It creates the IAM OIDC identity provider (pinned to Linro's issuer + audience),
the read-only scanner role, and the trust policy (pinned to your namespace) —
then outputs the exact AWS_CREDENTIALS config to hand back to Linro.
cd deploy/terraform/examples/onboarding-wif
terraform init
terraform apply \
-var linro_namespace=linro-acme # from Step 0
# linro_oidc_issuer / linro_audience default to Linro prod; override only
# if Step 0 reported different values.
# Already have an IAM OIDC provider for Linro's issuer? Reuse it:
# -var oidc_provider_name=arn:aws:iam::<acct>:oidc-provider/<issuer-host>
When it finishes:
terraform output -raw aws_credentials_config
Copy that JSON — it is not secret (it carries no key material, only a role
ARN) — and give it to your Linro operator for Step 2.
What to grant, and why
The plugin calls per-service Get*/List*/Describe* APIs directly (S3,
IAM, EC2 today; more services over time), so the scanner role needs broad read.
The Terraform default attaches two AWS managed policies:
SecurityAudit — read security-relevant configuration, including IAM
policies and settings the Get*Policy calls need, and
ViewOnlyAccess — broad List/Describe across services.
Prefer a single policy? Pass
-var 'permissions_policy_arns=["arn:aws:iam::aws:policy/ReadOnlyAccess"]'
(broader, but simpler). Do not attach write/admin policies — the plugin only
ever reads.
Step 2 — Linro operator wires it up
The operator sets one sealed sensor-profile environment variable on the AWS
sensor profile:
AWS_CREDENTIALS = <the aws_credentials_config JSON from Step 1>
e.g. {"type":"web_identity","role_arn":"arn:aws:iam::123456789012:role/linro-scanner",...}
The value is AES-GCM sealed, materialized into a per-profile Kubernetes Secret,
and mounted into the sensor pod (envFrom). The plugin reads it, validates it is
a web_identity config, and assumes the role via the projected token the pod
already mounts at /var/run/secrets/linro/federation/token. No plugin config and
no per-sensor setup are required.
Step 3 — Verify
Trigger a reconcile for the AWS plugin (UI: the plugin's sync action; CLI:
linro reconciliation ...). Within a minute, AWS resources (S3 buckets, IAM
users/roles, EC2 VPCs/security groups, …) appear in your inventory. If nothing
appears, see Troubleshooting.
Optional — Event sourcing (near-real-time inventory)
The steps above give you poll-based inventory: the sensor periodically
reconciles by listing/describing resources. Event sourcing adds
near-real-time updates — the sensor consumes CloudTrail events and
refreshes (or evicts) exactly the resource that changed, within seconds of the
change, instead of waiting for the next reconcile.
Event sourcing is additive and optional. Reconciliation always runs as the
eventual-consistency backstop; events just make the inventory fresher between
reconciles. It reuses the same WIF scanner identity from Steps 0–2 — there is
no second credential (grant the scanner role read on the CloudTrail S3 bucket /
SQS queue / Kinesis stream, below).
Four strategies ship. EventBridge is the recommended real-time path; S3 is the
zero-extra-infra fallback. See
deploy/terraform/README.md for the full
comparison and cost analysis.
| Strategy | Transport | Latency | Coverage | When to use |
|---|
eventbridge (recommended) | EventBridge rule → SQS (inline events) | seconds | Write events | Default. Real-time create/update/delete, low cost, no S3 fetch. |
sqs | CloudTrail → S3, SNS → SQS notify | 5–15 min | All (full log files) | Balanced; full event coverage incl. read events. |
s3 | CloudTrail → S3, sensor polls | 5–15 min | All | Zero extra infra; cost-sensitive. |
kinesis | CloudWatch Logs → Kinesis | seconds | All, ordered | Real-time + per-shard ordering (higher cost). |
A multi-region CloudTrail trail captures API calls from every region, and
global services (IAM, CloudFront, Route53, STS) always fire events in
us-east-1 — so start there. EventBridge rules are per-region: add regions
with examples/add-region/.
4a — Recommended: EventBridge (real-time)
What a human does (Console / Terraform): deploy the quickstart, which creates
a multi-region CloudTrail trail + S3 bucket + EventBridge rule → SQS queue:
cd deploy/terraform/examples/quickstart
terraform init && terraform apply # defaults to us-east-1
# other regions: cd ../add-region && terraform apply -var region=eu-west-1
Grant the scanner role sqs:ReceiveMessage, sqs:DeleteMessage,
sqs:GetQueueAttributes on the created queue (events are delivered inline — no
S3 access needed). See deploy/terraform/README.md → "Agent IAM Requirements".
4b — Fallback: S3 polling (no extra infra)
cd deploy/terraform/examples/strategies/s3
terraform init && terraform apply
Grant the scanner role s3:ListBucket on the bucket and s3:GetObject on
AWSLogs/*. The sensor lists + reads CloudTrail log objects directly.
4c — Register the event source in Linro
Once the AWS side is in place, create a Linro EventSource for the AWS plugin
and attach it to the AWS sensor profile. The type field is the strategy
name (eventbridge / sqs / s3 / kinesis); config is the strategy
config.
Config payloads:
// type: "eventbridge" (also "sqs")
{ "queueURL": "<sqs_queue_url output>", "region": "us-east-1", "accountID": "123456789012" }
// type: "s3"
{ "bucketName": "<s3_bucket_name output>", "prefix": "AWSLogs/", "region": "us-east-1", "accountID": "123456789012" }
// type: "kinesis" (one per shard)
{ "streamARN": "<kinesis_stream_arn output>", "shardID": "shardId-000000000000" }
UI: System → Events → Sources → New. Pick the AWS plugin, choose the
strategy, fill the config form (rendered from the strategy's JSON Schema), enable
it, then add the event source to the AWS sensor profile.
CLI (config is a struct, so pass the whole request via --from-file):
# 1. Create the event source. request.json:
# {
# "event_source_id": "aws-eventbridge-us-east-1",
# "event_source": {
# "title": "AWS events (EventBridge us-east-1)",
# "plugin": "plugins/<aws-plugin-id>",
# "type": "eventbridge",
# "enabled": true,
# "config": { "queueURL": "...", "region": "us-east-1", "accountID": "123456789012" }
# }
# }
linro data-source create-event-source --address https://<your-install-host> \
--from-file request.json
# → returns eventSources/<id>
# 2. Attach it to the AWS sensor profile. NOTE: --sensor-profile.event-sources
# REPLACES the whole set — first read the profile's current event sources
# (get-sensor-profile), then pass ALL of them plus the new id, or you will
# detach the existing ones:
linro sensor-profile update-sensor-profile --address https://<your-install-host> \
--sensor-profile.event-sources eventSources/<existing-1> \
--sensor-profile.event-sources eventSources/<id>
Attaching is required: the sensor only runs Produce for event sources bound to
its profile. After attach, the sensor opens the stream within ~one heartbeat;
mutations in AWS then appear in inventory without waiting for the next reconcile.
Onboarding from an agent? The event-sourcing setup is restated as
structured steps (strategy decision rule, concrete calls, idempotency) in
onboarding-agent.md.
Troubleshooting
| Symptom | Cause | Fix |
|---|
AccessDenied / InvalidIdentityToken on assume | Namespace/audience/issuer mismatch | Re-read Step 0 from the install; the trust policy :sub condition must equal system:serviceaccount:<your-namespace>:* exactly; :aud and the OIDC provider client-id must include the audience |
Not authorized to perform sts:AssumeRoleWithWebIdentity | Trust policy principal wrong | The role's Federated principal must be the IAM OIDC provider ARN for Linro's issuer |
All resources of a service AccessDenied | Missing read permission | Confirm SecurityAudit + ViewOnlyAccess (or ReadOnlyAccess) are attached to the scanner role |
getIamPolicy-style calls fail but others pass | ViewOnlyAccess alone doesn't cover policy reads | Ensure SecurityAudit is attached (it grants the IAM policy reads) |
| OIDC provider creation fails / issuer unreachable | Private/non-public cluster issuer | WIF requires a publicly reachable issuer (AWS fetches JWKS itself); confirm by curling the oidc_discovery_url from GetCloudFederationInfo. Linro production uses a public EKS issuer |
| Event source enabled but no real-time updates | Event source not attached to the sensor profile | Add eventSources/<id> to the AWS sensor profile's event_sources — the sensor only streams attached sources |
| EventBridge source connects but stays empty | Rule filters readOnly=false (write events only), or no mutations occurred | Trigger a write (e.g. tag a resource); use s3/sqs for full coverage |
| S3 / SQS source lands nothing | Scanner role lacks s3:GetObject/s3:ListBucket or sqs:ReceiveMessage, wrong bucketName/queueURL/region | Grant the IAM in deploy/terraform/README.md; verify the config values |
Onboarding from an AI agent
If an automation agent (Claude or any MCP-aware agent) is driving the
onboarding, follow onboarding-agent.md instead — it
restates this entire flow (trust creation, the AWS_CREDENTIALS output, the
operator's step, verification, and event sourcing) as ordered, structured steps
with concrete API calls and the questions the agent should ask you up-front.
Linro's MCP serves that guide to a driving agent via get_plugin_onboarding.
See docs/authentication.md for the credential
resolution + validation details.