
Identity-Driven Access to Internal Resources Using AWS SSM
Zero-trust access to internal AWS resources using IAM Identity Center, SSM port forwarding and ABAC.
Traditional access to private AWS infrastructure usually relies on VPN connectivity and direct subnet reachability.
It works, but it also increases attack surface, enables lateral movement, and makes auditing harder.
It works, but it also increases attack surface, enables lateral movement, and makes auditing harder.
This article describes a different approach:
An identity-driven, zero-trust access model built on:
- AWS IAM Identity Center (SSO + MFA)
(Optional) - AWS Systems Manager Session Manager port forwarding
- Attribute-Based Access Control (ABAC)
No direct subnet access.
No shared passwords.
No SSH key distribution.
No shared passwords.
No SSH key distribution.
Everything is controlled by identity.
Overview
This solution introduces a unified, identity-based access model for developers and SREs to securely reach internal AWS VPC resources without relying on traditional VPN connectivity.
Instead of granting broad network-level access via VPN or routing into private subnets, engineers authenticate through IAM Identity Center and access internal services using SSM Session Manager.
Access becomes:
- Explicit
- Per-user
- Per-session
- Fully auditable
Users do not join the network.
They establish tightly-scoped, IAM-authorized sessions.
They establish tightly-scoped, IAM-authorized sessions.
Key Benefits
1. Eliminates VPN Dependency
This model can fully replace VPN-based workflows.
Instead of giving engineers full private network access, they use SSM-managed tunnels:
- No inbound connectivity required
- No client-side routing configuration
- No exposed SSH
- Access granted only via IAM authorization
VPN access becomes unnecessary for engineering tasks.
2. Zero Trust Model (Identity > Network)
Traditional VPN provides broad reachability once connected.
This introduces risk:
- Flat network access
- Shared environment exposure
- Weak audit traceability
With SSM + ABAC:
- Access is per-session
- Controlled via IAM
- Fully logged in CloudTrail
- Restricted to approved documents
The control plane is AWS-managed, not network-based.
3. Strong ABAC Enforcement
Identity Center injects session attributes such as:
aws:PrincipalTag/username
These tags drive authorization across:
- SSM session documents
- Bastion EC2 targets
- S3 prefixes
- RDS IAM users
- and many more
ABAC removes the need for per-user IAM policy management and scales cleanly.
4. Controlled Port Forwarding
Port forwarding is allowed only through approved SSM documents matching:
PF-${aws:PrincipalTag/username}-*This ensures:
- No arbitrary documents
- No unrestricted shell sessions
- Host and port are validated
- Session behavior is centrally governed
Access is controlled at the session layer, not the network layer.
5. IAM Authentication for RDS (as an example)
Users connect to RDS using IAM database authentication.
Each user maps to:
dbuser/${aws:PrincipalTag/username}Access granted via:
rds-db:connectThis eliminates:
- Shared passwords
- Static credentials
- Manual password rotation
Database access becomes identity-based and traceable.
6. Per-User Isolation in S3 (as an example)
The same ABAC model applies to S3.
Users are restricted to:
s3://bucket/${aws:PrincipalTag/username}/...This provides:
- Strong per-user isolation
- No need for multiple buckets
- Clear ownership
- Automatic least privilege
7. Reduced Network Exposure
Under this architecture:
- Engineers do not access private subnets directly
- No inbound SSH
- No broad VPC routing rules
Only approved access paths are available.
Lateral movement risk is dramatically reduced.
Lateral movement risk is dramatically reduced.
8. Auditability and Compliance
Every access path is logged:
- CloudTrail logs StartSession
- Session documents define approved paths
- RDS IAM auth provides per-user attribution
- S3 logs align with principal identity
You can answer:
- Who connected
- When
- To which resource
- Through which document
Security Controls
Restrict StartSession by Source IP
You can deny ssm:StartSession unless requests originate from approved public IPs (for example via SCP):
1
2
3
4
5
6
7
8
9
10
{
"Effect": "Deny",
"Action": "ssm:StartSession",
"Resource": "*",
"Condition": {
"NotIpAddress": {
"aws:SourceIp": ["123.231.234.123/32", "111.222.111.222/32"]
}
}
}Enforce Session Timeouts and Logging
SSM Session Manager supports session-level controls that limit exposure and improve auditability:
- Idle timeout: Automatically terminate inactive sessions (e.g., 2 minutes)
- Max session duration: Hard limit on session length (e.g., 60 minutes)
- Session logging: Stream session output to S3 or CloudWatch Logs
These settings are configured via Session Manager preferences (AWS Console, CLI, or Terraform) and apply to all sessions in the account or region.
Operational Simplicity
This architecture removes:
- VPN provisioning
- SSH key management
- Bastion exposure
- Shared DB credentials
- Per-user IAM policy sprawl
Access is managed through:
- Identity Center groups
- Session tags
- ABAC policies
- Centralized Session Manager documents
Implementation
[NOTE]
Replaceaws:PrincipalTag/usernamewith${aws:userid}if you do not use SSO.
Step 1. Configure IAM Identity Center for ABAC (SSO only)
Enable session tags in Identity Center and configure user attribute mapping (Identity Center -> Settings -> Attributes for access control):
1
username = ${path:userName}This becomes
aws:PrincipalTag/username and drives all downstream authorization.Example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{
"eventVersion": "1.11",
"userIdentity": {
"type": "SAMLUser",
"principalId": "4xxxxxxxxxxxxxxxxxxxxxxxxxg=:serhii.kaidalov@xxx.xxx",
"userName": "serhii.kaidalov@xxx.xxx",
"identityProvider": "4xxxxxxxxxxxxxxxxxxxxxxxxxg="
},
"eventTime": "2026-02-26T01:00:26Z",
"eventSource": "sts.amazonaws.com",
"eventName": "AssumeRoleWithSAML",
"awsRegion": "us-east-1",
"sourceIPAddress": "123.123.123.123",
"userAgent": "aws-internal/3 aws-sdk-java/1.12.797 Linux/4.14.355-280.714.amzn2.x86_64 OpenJDK_64-Bit_Server_VM/17.0.18+9-LTS java/17.0.18 vendor/Amazon.com_Inc. cfg/retry-mode/standard cfg/auth-source#imds m/N,P",
"requestParameters": {
"sAMLAssertionID": "_cxxxxxxc-xxxx-xxxx-xxxx-cxxxxxxxxxxxb",
"roleSessionName": "serhii.kaidalov@xxx.xxx",
"principalTags": {
"username": "serhii.kaidalov"
},
"roleArn": "arn:aws:iam::123456789012:role/aws-reserved/sso.amazonaws.com/AWSReservedSSO_AdministratorAccess_3xxxxxxxxxxxxxx4",
"principalArn": "arn:aws:iam::123456789012:saml-provider/AWSSSO_6xxxxxxxxxxxxxx5_DO_NOT_DELETE",
"durationSeconds": 28800
},
...Step 2. Create SSM Bastion Instance
Deploy a lightweight EC2 instance inside private VPC with:
- SSM Agent installed
- No inbound SSH
- Private subnet + SSM interface endpoint
(optional but recommended) - IAM instance role allowing SSM
- Tag:
SSMBastion = true
Security groups should only allow outbound traffic to required internal resources (e.g., RDS).
Step 3. Restrict StartSession to Tagged Instances
1
2
3
4
5
6
7
8
9
10
11
{
"Sid": "StartSessionOnlyToTaggedBastions",
"Effect": "Allow",
"Action": "ssm:StartSession",
"Resource": "arn:aws:ec2:*:*:instance/*",
"Condition": {
"StringEquals": {
"ssm:resourceTag/SSMBastion": "true"
}
}
}Sessions can only target approved bastion instances. You can be more specific with user policy and add account, region, etc.
Step 4. Restrict SSM Documents Per User
Enforce naming convention
PF-${aws:PrincipalTag/username}-*:1
2
3
4
5
6
{
"Sid": "AllowOnlyPerUserSessionDocuments",
"Effect": "Allow",
"Action": ["ssm:StartSession", "ssm:GetDocument"],
"Resource": "arn:aws:ssm:*:*:document/PF-${aws:PrincipalTag/username}-*"
}Users cannot use generic
AWS-StartPortForwardingSession document, only documents specifically generated for them.Step 5. Example SSM Session Document (MySQL)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
{
"schemaVersion": "1.0",
"description": "Port forwarding to RDS MySQL only (3306)",
"sessionType": "Port",
"parameters": {
"portNumber": {
"type": "String",
"allowedPattern": "^3306$",
"default": "3306"
},
"localPortNumber": {
"type": "String",
"allowedPattern": "^([0-9]{1,5})$",
"default": "0"
},
"host": {
"type": "String",
"allowedPattern": "^rds-cluster\\.cluster-xxxx\\.us-east-1\\.rds\\.amazonaws\\.com$"
}
},
"properties": {
"type": "LocalPortForwarding",
"portNumber": "{{ portNumber }}",
"localPortNumber": "{{ localPortNumber }}",
"host": "{{ host }}"
}
}This ensures only MySQL (3306) and only approved RDS hostname, no arbitrary hosts.
Step 6. Allow SSM Channel Permissions
Required for Session Manager:
1
2
3
4
5
6
7
8
9
10
11
{
"Sid": "AllowSSMMessagesChannels",
"Effect": "Allow",
"Action": [
"ssmmessages:CreateControlChannel",
"ssmmessages:CreateDataChannel",
"ssmmessages:OpenControlChannel",
"ssmmessages:OpenDataChannel"
],
"Resource": "*"
}Step 7. Enforce RDS IAM Authentication
Each database user must match IAM identity:
1
2
3
4
5
6
{
"Sid": "AllowRdsIamConnectOnlyAsOwnDbUser",
"Effect": "Allow",
"Action": "rds-db:connect",
"Resource": "arn:aws:rds-db:*:123456789012:dbuser:*/${aws:PrincipalTag/username}"
}User can only connect as their own DB user. Again, you can be more specific here and specify exact region, account, database cluster/instance
Step 8. Enforce S3 Per-User Prefix Isolation (as an example)
1
2
3
4
5
6
7
8
9
{
"Sid": "S3RWOnlyOwnPrefix",
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
"Resource": [
"arn:aws:s3:::bucket-name/${aws:PrincipalTag/username}",
"arn:aws:s3:::bucket-name/${aws:PrincipalTag/username}/*"
]
}Optional prefix-restricted listing:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"Sid": "S3ListOnlyOwnPrefix",
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::bucket-name",
"Condition": {
"StringLike": {
"s3:prefix": [
"${aws:PrincipalTag/username}",
"${aws:PrincipalTag/username}/*"
]
}
}
}Step 9. Restrict Session Lifecycle to Own Sessions
1
2
3
4
5
6
{
"Sid": "AllowOnlyOwnSessionsLifecycle",
"Effect": "Allow",
"Action": ["ssm:TerminateSession", "ssm:ResumeSession"],
"Resource": "arn:aws:ssm:*:*:session/${aws:userid}-*"
}Users cannot terminate or resume other users' sessions.
Example: Developer Workflow
Login
aws sso login --profile ssm-profile or aws login --profile ssm-profileVerify credentials:
1
aws sts get-caller-identity --profile ssm-profileStart Port Forwarding
1
aws ssm start-session --region us-east-1 --target i-xxxxxxxxxxxx --document-name PF-serhii.kaidalov-my-cluster-3306 --parameters host="my-cluster.cluster-xxxxxxxx.us-east-1.rds.amazonaws.com",portNumber="3306",localPortNumber="3306"Keep the terminal open, this is your secure tunnel.
Connect to RDS via IAM
1
2
3
4
5
6
7
8
DB_HOST="my-cluster.cluster-xxxxxxxx.us-east-1.rds.amazonaws.com"
DB_PORT=3306
DB_USER=serhii.kaidalov
LOCAL_PORT=3306
TOKEN="$(aws rds generate-db-auth-token --region us-east-1 --hostname "$DB_HOST" --port "$DB_PORT" --username "$DB_USER")"
mysql --host 127.0.0.1 --port "$LOCAL_PORT" --user "$DB_USER" --password="$TOKEN" --enable-cleartext-plugin --ssl-mode=REQUIREDNo static credentials.
No password storage.
No password storage.
Just-in-Time Access with Slack Integration
Managing SSM documents manually works for small teams, but as your organization grows, you may want a self-service workflow with approval gates.
SSM Just-in-Time Access is a Slack-integrated system that automates this process:
- User requests access via
/ssm-accessSlack command - Two-tier approval: security team and manager approve via Slack (using Slack user groups)
- Automatic provisioning: once approved, the system creates a time-limited SSM document with ABAC tags
- Detailed instructions: user receives connection instructions directly in Slack
- Automatic cleanup: documents expire and are removed after the configured duration
This approach adds:
- Self-service access requests
- Approval workflows with audit trail
- Time-bound access (e.g., 14 days)
- No manual IAM or SSM document management
The system integrates with IAM Identity Center and relies on Slack user groups for approver management (requires a paid Slack plan).
Conclusion
This model replaces traditional network-based access with identity-driven authorization.
Instead of asking:
"Are you on the VPN?"
We ask:
"Are you authorized for this specific resource, through this approved document, right now?"
By combining:
- IAM Identity Center for centralized authentication
- SSM Session Manager for secure tunneling without exposed ports
- ABAC policies for scalable, tag-based authorization
- IAM database authentication for credential-free RDS access
- Just-in-time provisioning for time-bound, auditable access
You get:
- A true zero-trust access model
- Full audit trail in CloudTrail
- No VPN, no SSH keys, no shared credentials
- Automatic least privilege via session tags
It is not only more secure, it is also operationally simpler.
And simplicity is one of the strongest forms of security.
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article