Skip to content

Vectasec · Guides

Scan AWS with a read-only role

Connect an AWS account to Vectasec through a cross-account read-only role and scan SQS, SNS, EventBridge, MSK and API Gateway across regions.

On this page

This guide connects one AWS account to Vectasec through a cross-account, read-only IAM role. You create the role, save a connection that assumes it, run a scan and read the findings. It assumes you already have a working engine and schema from the Quick start.

What gets scanned#

One aws connection covers one account. When role_arn is set, the connector first assumes that role. It then calls sts:GetCallerIdentity to prove the credential works, and fans out over every region you list in regions to read:

  • SQS: queues and their attributes (resource policy, encryption, dead-letter queue)
  • SNS: topics, topic attributes and subscriptions
  • EventBridge: event buses and their policies, rules and rule targets
  • MSK: clusters (client authentication, encryption, public access, monitoring level)
  • API Gateway: v1 REST APIs and v2 HTTP/WebSocket APIs (routes, authorizers, stages)

Apart from the STS calls (AssumeRole and GetCallerIdentity), every call is a List*, Get* or Describe* read. No call sends, receives or reads a message. EventBridge target input payloads are not stored, because they can embed message bodies.

The scan produces these resource kinds: aws_account, aws_sqs_queue, aws_sns_topic, aws_sns_subscription, aws_event_bus, aws_event_rule, aws_event_target, aws_msk_cluster, aws_api and aws_api_stage. Principals named in the Allow statements of SQS, SNS and EventBridge resource policies become non-human identities with grants. For more on those, see Non-human identities and attack paths.

1. Create a read-only role in the target account#

Create a role in the account you want to scan. Its trust policy should allow only the principal that runs your scans, and it should require an external ID. Save the policy as trust-policy.json:

json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": { "AWS": "arn:aws:iam::111122223333:role/vectasec-engine" },
      "Action": "sts:AssumeRole",
      "Condition": {
        "StringEquals": { "sts:ExternalId": "your-external-id" }
      }
    }
  ]
}

Replace 111122223333:role/vectasec-engine with the IAM principal your scans run as. The connector makes the AssumeRole call using the standard AWS credential chain of whichever process runs it:

  • the engine API when you click Test in the console
  • vectasec worker when you click Scan in the console, and for scheduled rescans (for a connection in the default cloud mode)
  • your own shell when you run the CLI

If these processes run as different IAM principals, trust each one in the policy. If the connection also sets profile or a key pair, that identity makes the call instead. When the calling principal lives in a different AWS account from the role, its own identity policy must also allow sts:AssumeRole on the new role.

Generate the external ID once, for example with openssl rand -hex 16, and keep it for step 2. It guards against the confused-deputy problem; it is not a password. Vectasec stores it with the connection's regular config.

Create the role and attach the AWS managed SecurityAudit policy:

bash
aws iam create-role \
  --role-name vectasec-readonly \
  --assume-role-policy-document file://trust-policy.json

aws iam attach-role-policy \
  --role-name vectasec-readonly \
  --policy-arn arn:aws:iam::aws:policy/SecurityAudit

SecurityAudit covers every call the connector makes. ReadOnlyAccess also works, but it grants much broader read access than the connector needs. If you write a custom policy instead, the connector calls only these actions (sts:GetCallerIdentity needs no permission):

  • SQS: sqs:ListQueues, sqs:GetQueueAttributes
  • SNS: sns:ListTopics, sns:GetTopicAttributes, sns:ListSubscriptionsByTopic
  • EventBridge: events:ListEventBuses, events:DescribeEventBus, events:ListRules, events:ListTargetsByRule
  • MSK: kafka:ListClustersV2
  • API Gateway (v1 and v2): apigateway:GET

2. Add the connection#

From the engine directory of the release, save the connection and test it:

bash
uv run vectasec connections add --type aws --name prod-aws \
  --set regions=us-east-1,eu-west-1 \
  --set role_arn=arn:aws:iam::123456789012:role/vectasec-readonly \
  --set external_id=your-external-id

uv run vectasec connections test prod-aws

The AWS fields have no dedicated CLI flags, so each one goes through --set key=value. connections test runs the full collection with the same connector code a scan uses and prints the resource count by kind. It stores nothing except the connection's status.

In the console, open Systems, choose Connect a system, then Amazon Web Services. Connecting a system needs the editor or admin role. The console shows the same fields, and the secret ones use password inputs.

FieldRequiredPurpose
regionsyesComma-separated regions to scan
role_arnnoRole to assume: the agentless path
external_idnoMust match sts:ExternalId in the trust policy
profilenoNamed profile from the AWS shared config on the host that runs the scan
access_key_idnoKey ID of a static key pair; stored with the regular config
secret_access_keyno, secretSecret half of the key pair; stored in Supabase Vault
session_tokenno, secretOnly for temporary STS credentials; stored in Supabase Vault

The base identity is chosen in this order: the key pair (when both access_key_id and secret_access_key are set), then the named profile, then the default chain. If role_arn is set, the connector assumes the role on top of that identity.

Prefer the role. It leaves no AWS secret in Vectasec at all, and its temporary credentials are held only in memory while the collection runs. If you must use static keys, enter them in the console rather than with --set, so they stay out of your shell history. Secret fields are kept in one Vault secret per connection and merged into memory only at scan or test time.

In CloudTrail, calls made through the role appear under the session name vectasec-readonly-scan.

3. Scan and review#

bash
uv run vectasec scan --connection prod-aws
uv run vectasec findings
uv run vectasec identities

scan prints resource, edge, finding and observation counts. findings lists every finding for the tenant, worst first, with evidence counts. In the console, Scan queues the same scan for vectasec worker. You can also set the connection to rescan automatically every 30 minutes, 1 hour, 6 hours, 24 hours or 7 days. Scheduled rescans need pg_cron in the database, and they skip a connection whose last test failed until a test passes again.

The 21 AWS rules:

RuleSeverityFires when
AWS.SQS.QUEUE_POLICY_PUBLICcriticalA queue policy allows Principal * with no Condition
AWS.SNS.TOPIC_POLICY_PUBLICcriticalA topic policy allows Principal * with no Condition
AWS.EVENTBRIDGE.BUS_POLICY_PUBLICcriticalA bus policy allows Principal * with no Condition
AWS.SQS.POLICY_WILDCARD_ACTIONhighA queue policy allows * or sqs:*
AWS.SNS.POLICY_WILDCARD_ACTIONhighA topic policy allows * or sns:*
AWS.EVENTBRIDGE.BUS_POLICY_WILDCARD_ACTIONhighA bus policy allows * or events:*
AWS.MSK.UNAUTHENTICATED_ACCESShigh, critical if publicUnauthenticated client access is enabled
AWS.MSK.CLIENT_PLAINTEXT_IN_TRANSIThighClient-to-broker traffic allows plaintext
AWS.MSK.PUBLIC_ACCESS_ENABLEDhighBrokers have public endpoints
AWS.APIGW.ROUTE_NO_AUTHORIZERhighA route has no authorizer and no API key (OPTIONS is skipped)
AWS.SQS.ENCRYPTION_AT_REST_OFFmediumNeither SSE-SQS nor a KMS key is set
AWS.SNS.ENCRYPTION_AT_REST_OFFmediumThe topic has no KMS key
AWS.SNS.SUBSCRIPTION_PLAINTEXT_HTTPmediumA subscription delivers over http
AWS.EVENTBRIDGE.CROSS_ACCOUNT_TARGETmediumA rule targets a resource in another account
AWS.MSK.INTERBROKER_PLAINTEXTmediumReplication between brokers is unencrypted (provisioned clusters)
AWS.APIGW.ROUTE_API_KEY_ONLYmediumAn API key is the route's only access control
AWS.SQS.NO_DEAD_LETTER_QUEUElow, info without a resource policyThe queue has no redrive policy (queues named as DLQs are skipped)
AWS.MSK.ENHANCED_MONITORING_DEFAULTlowEnhanced monitoring is DEFAULT (provisioned clusters)
AWS.APIGW.STAGE_LOGGING_DISABLEDlowThe stage has no access log and execution logging is off
AWS.APIGW.STAGE_NO_WAFlowA REST API stage has no WAF web ACL
AWS.ACCOUNT.SERVICE_NOT_ASSESSEDinfoA service or child listing could not be read

A wildcard principal scoped by a Condition, such as aws:SourceArn or aws:PrincipalOrgID, is not reported as public. Each finding cites the policy statement, field or route it read. For how findings are recorded, see Findings, evidence and compliance.

Permissions and coverage gaps#

A denied read never counts as a pass. It becomes a not_assessable finding that names what could not be read:

What could not be readHow it shows up
A whole service's list call in one region, deniedAWS.ACCOUNT.SERVICE_NOT_ASSESSED, naming the service, region and error
SNS subscriptions, EventBridge rules, or API Gateway stages or authorizersAWS.ACCOUNT.SERVICE_NOT_ASSESSED, naming the parent resources and the missing children
sqs:GetQueueAttributes or sns:GetTopicAttributes for one resourceThe public-policy and encryption rules on that queue or topic
events:DescribeEventBusAWS.EVENTBRIDGE.BUS_POLICY_PUBLIC on that bus
events:ListTargetsByRuleAWS.EVENTBRIDGE.CROSS_ACCOUNT_TARGET on that rule
An API's route listingAWS.APIGW.ROUTE_NO_AUTHORIZER on that API

vectasec posture counts these findings as coverage gaps. To close one, widen the role with the read actions the finding names and rescan. AWS rules are not mapped to compliance framework controls today, so AWS findings, including these gaps, do not change a control's status.

A failed AssumeRole or GetCallerIdentity call is an authentication failure and fails the whole scan. A service call that fails for another reason, such as an unreachable regional endpoint, is recorded in the collection field of the aws_account resource. Check that field if a region returns no resources for a service.

Not covered today#

  • Step Functions, Lambda and Kinesis
  • ElastiCache and MemoryDB
  • IAM inventory beyond the principals that resource policies name. The connector makes no iam:List* calls.
  • AWS Organizations enumeration. The account and regions are explicit config, so you add one connection per account.
  • CloudTrail or usage telemetry, so no last-used or staleness data

For MSK, this connector reads the managed cluster properties. Broker-level Kafka posture comes from the Kafka connector. That connector opens its admin client with no SASL or TLS settings, so today it can reach only a plaintext listener that accepts unauthenticated clients. It cannot scan MSK listeners that require TLS, IAM or SCRAM.