Connect with an AWS account
Create the read-only AWS identity Valkan uses, then connect and scan the account.
Complete these steps in the AWS account that Valkan will scan, before choosing Connect AWS in Valkan.
You create the IAM user and role; Valkan never creates or changes AWS resources.
You need permission to create:
- IAM users
- roles
- access keys
- an inline policy on the bootstrap user (step 1)
- a customer-managed policy, attached to the role (step 3)
The recommended setup has two identities:
valkan-bootstrap: a programmatic IAM user. Its access key can only assume the role below.ValkanDiscoveryReadOnly: an IAM role with the read-only permissions Valkan needs. This is an example name, not an AWS-managed or Valkan-created role.
1. Create the bootstrap user
- In AWS Console, open IAM → Users → Create user.
- Name it
valkan-bootstrap. - Do not give it console access.
- Create an access key under Security credentials → Create access key → Third-party service.
- Save the Access Key ID and Secret Access Key; AWS displays the secret only once.
- On the user's Permissions tab, create an inline policy.
- Replace
<account-id>with the 12-digit AWS account ID and paste:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Resource": "arn:aws:iam::<account-id>:role/ValkanDiscoveryReadOnly"
}]
}This user cannot inspect your AWS resources. It can only obtain a temporary session for the specific read-only role.
2. Create the read-only role
- Choose IAM → Roles → Create role → Custom trust policy.
- Generate a random External ID (a UUID is suitable).
- Save it in your password manager.
- Replace the placeholders below, and paste this trust policy:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::<account-id>:user/valkan-bootstrap" },
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": { "sts:ExternalId": "<your-random-external-id>" }
}
}]
}Name the role ValkanDiscoveryReadOnly.
This trust policy only controls who can assume the role; it does not give the role AWS read permissions.
3. Give the role its read-only permissions
Create this as a customer-managed policy, not an inline one, and attach it to the role:
- Go to IAM → Policies → Create policy → JSON.
- Paste the policy below.
- Name it
ValkanDiscoveryReadOnlyPolicy. - Open the role and choose Add permissions → Attach policies.
Why a managed policy. This policy is about 2,700 non-whitespace characters. A customer-managed policy allows 6,144, can be versioned and reused, and is what AWS recommends over inline policies. Do not attach it to the
valkan-bootstrapuser: IAM caps a user's inline policies at 2,048 characters in total.
It does not allow create, update, delete, invoke, or any other write operation.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ValkanDiscovery",
"Effect": "Allow",
"Action": [
"sts:GetCallerIdentity",
"organizations:DescribeOrganization",
"organizations:ListAccounts",
"organizations:ListParents",
"organizations:ListPoliciesForTarget",
"organizations:DescribePolicy",
"iam:ListRoles",
"iam:GetRole",
"iam:GetInstanceProfile",
"iam:ListUsers",
"iam:ListGroups",
"iam:ListAccessKeys",
"iam:GetAccessKeyLastUsed",
"iam:ListMFADevices",
"iam:ListPolicies",
"iam:GetPolicy",
"iam:GetPolicyVersion",
"iam:ListEntitiesForPolicy",
"iam:ListAttachedRolePolicies",
"iam:ListRolePolicies",
"iam:GetRolePolicy",
"iam:ListAttachedUserPolicies",
"iam:ListUserPolicies",
"iam:GetUserPolicy",
"iam:ListUserTags",
"iam:ListRoleTags",
"iam:ListGroupsForUser",
"iam:ListAttachedGroupPolicies",
"iam:ListGroupPolicies",
"iam:GetGroupPolicy",
"bedrock:ListFoundationModels",
"bedrock:ListCustomModels",
"bedrock:ListProvisionedModelThroughputs",
"bedrock:ListGuardrails",
"bedrock:GetModelInvocationLoggingConfiguration",
"bedrock:ListAgents",
"bedrock:GetAgent",
"bedrock:ListAgentActionGroups",
"bedrock:GetAgentActionGroup",
"bedrock:ListAgentKnowledgeBases",
"bedrock:ListAgentCollaborators",
"bedrock:ListTagsForResource",
"bedrock-agentcore:ListAgentRuntimes",
"bedrock-agentcore:GetAgentRuntime",
"bedrock-agentcore:ListGateways",
"bedrock-agentcore:GetGateway",
"bedrock-agentcore:ListGatewayTargets",
"bedrock-agentcore:GetGatewayTarget",
"sagemaker:ListEndpoints",
"sagemaker:DescribeEndpoint",
"sagemaker:ListModels",
"qbusiness:ListApplications",
"qbusiness:ListRetrievers",
"qbusiness:ListDataSources",
"qbusiness:ListWebExperiences",
"lambda:ListFunctions",
"lambda:GetFunction",
"ecs:ListClusters",
"ecs:DescribeServices",
"ecs:DescribeTaskDefinition",
"events:DescribeEventBus",
"cloudtrail:ListTrails",
"cloudtrail:DescribeTrails",
"cloudtrail:GetEventSelectors",
"cloudtrail:LookupEvents",
"cloudwatch:ListMetrics",
"cloudwatch:GetMetricData",
"s3:ListAllMyBuckets",
"dynamodb:ListTables",
"secretsmanager:ListSecrets",
"identitystore:GetUserId",
"identitystore:DescribeUser",
"route53resolver:ListResolverQueryLogConfigs",
"route53resolver:ListResolverQueryLogConfigAssociations",
"ec2:DescribeVpcs",
"ec2:DescribeNetworkInterfaces",
"ec2:DescribeInstances",
"ec2:DescribeSecurityGroups",
"ec2:DescribeRouteTables",
"ec2:DescribeVpcPeeringConnections",
"elasticloadbalancing:DescribeLoadBalancers",
"elasticloadbalancing:DescribeTargetGroups",
"elasticloadbalancing:DescribeTargetHealth",
"elasticloadbalancing:DescribeListeners",
"route53:ListHostedZones",
"route53:ListResourceRecordSets",
"eks:ListClusters",
"eks:DescribeCluster",
"eks:ListPodIdentityAssociations",
"eks:DescribePodIdentityAssociation",
"rds:DescribeDBInstances",
"rds:DescribeDBClusters",
"redshift:DescribeClusters",
"es:ListDomainNames"
],
"Resource": "*"
},
{
"Sid": "ValkanDnsQueryLogs",
"Effect": "Allow",
"Action": [
"logs:FilterLogEvents"
],
"Resource": [
"arn:aws:logs:*:*:log-group:/aws/route53resolver/*",
"arn:aws:logs:*:*:log-group:/aws/route53resolver/*:*",
"arn:aws:logs:*:*:log-group:/valkan/*",
"arn:aws:logs:*:*:log-group:/valkan/*:*"
]
}
]
}Two groups here are optional, and Valkan degrades rather than fails without them:
- The managed-database actions (
rds:DescribeDBInstances,rds:DescribeDBClusters,redshift:DescribeClusters) cost you the Resources ▸ Databases inventory and the links from identities and agents to your databases; the scan itself still completes. - Dropping
iam:ListGroupsForUsercosts group-inherited permissions — and since granting admin through a group is common, a user who is an admin that way will read as having no permissions at all.
Each missing grant is reported as a permission gap rather than an error, so you can add them later and rescan.
One rds: grant covers RDS, Aurora, and standard DocumentDB and Neptune clusters: those answer on the RDS control plane, so there is no separate docdb: or neptune: permission to ask for. DocumentDB Elastic clusters and Neptune Analytics graphs are separate AWS services (docdb-elastic:, neptune-graph:) that this policy does not cover, so Valkan does not scan them.
Tags come back with the describe calls, so no tag-reading grant is needed either.
On a new or unused AWS account, the IAM console may reject newer Bedrock action names even though the policy is valid. If that happens, ask an AWS CLI user to attach the same policy, or open the Bedrock console and retry after its service catalog refreshes.
4. Connect and scan in Valkan
In Valkan, choose Integrations → Sources → Connect AWS. Select Use a role to assume, then provide:
- Role ARN:
arn:aws:iam::<account-id>:role/ValkanDiscoveryReadOnly - External ID: the value generated in step 2
- Bootstrap access key ID and secret: the values from step 1
- Region: the AWS region to begin scanning
- Choose Validate & continue.
- Resolve any missing permission probes.
- Complete the organization step and choose Scan now.
A scan can finish with permission gaps, but it will not have complete coverage until those gaps are resolved.
5. (Optional) See how VMs are reached from the internet
The role policy in step 3 includes the read-only actions that show how each VM is reached: ec2:DescribeSecurityGroups, ec2:DescribeRouteTables, ec2:DescribeVpcPeeringConnections, elasticloadbalancing:DescribeLoadBalancers, DescribeTargetGroups, DescribeTargetHealth and DescribeListeners, and route53:ListHostedZones and ListResourceRecordSets. Valkan reads security group rules open to the internet, whether the subnet routes to an internet gateway, the internet-facing load balancers that target the VM, the Route 53 names that point at it, and which databases it can connect to across VPC peering and security group rules. If you attached the policy before these were added, attach it again; until then a VM shows its exposure as unknown, with a permission gap per missing action, never as closed. Network ACLs, host firewalls and WAF rules are not evaluated.
6. (Optional) Enable DNS visibility
Powers the DNS tab; off by default in AWS.
The role policy in step 3 already includes the five read-only actions it needs. Run this once per VPC whose workloads you want logged:
aws logs create-log-group --log-group-name /aws/route53resolver/query-logs/<vpc-id>
# 1-day retention keeps CloudWatch Logs storage cost down; hourly scans never need more.
aws logs put-retention-policy \
--log-group-name /aws/route53resolver/query-logs/<vpc-id> --retention-in-days 1
aws route53resolver create-resolver-query-log-config \
--name valkan-dns-logging-<vpc-id> \
--destination-arn "arn:aws:logs:<region>:<account-id>:log-group:/aws/route53resolver/query-logs/<vpc-id>"
aws route53resolver associate-resolver-query-log-config \
--resolver-query-log-config-id <CONFIG_ID> --resource-id <vpc-id>You don't have to hunt for the VPC list — scan first, and Valkan raises a finding per unlogged VPC with these commands pre-filled.
- The destination must be CloudWatch Logs (S3 and Firehose configs are skipped).
- What you get is instance → provider; DNS carries no process, identity, or volume, and EKS pods attribute to the node rather than the pod.
7. (Optional) Enable Bedrock model invocation logging
Standard Bedrock Runtime calls are observed from CloudTrail as hourly caller-to-model aggregates.
- Valkan does not read prompts or responses, even if you enable invocation logging.
- Invocation logging is currently a posture signal only; Valkan can't turn it on because it is a write.
Run once per region:
aws logs create-log-group --log-group-name /aws/bedrock/model-invocations
aws iam create-role --role-name BedrockLoggingRole \
--assume-role-policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"bedrock.amazonaws.com"},"Action":"sts:AssumeRole"}]}'
aws iam put-role-policy --role-name BedrockLoggingRole --policy-name BedrockLogsWrite \
--policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["logs:CreateLogStream","logs:PutLogEvents"],"Resource":"arn:aws:logs:<region>:<account-id>:log-group:/aws/bedrock/model-invocations:*"}]}'
aws bedrock put-model-invocation-logging-configuration --logging-config '{
"cloudWatchConfig": {"logGroupName": "/aws/bedrock/model-invocations", "roleArn": "arn:aws:iam::<account-id>:role/BedrockLoggingRole"},
"textDataDeliveryEnabled": true
}'Bedrock needs its own assumable role here (unlike DNS logging) because Bedrock itself writes the logs, not you.