diff --git a/cloudformation/ssm-relay/README.md b/cloudformation/ssm-relay/README.md new file mode 100644 index 0000000..49d8a1e --- /dev/null +++ b/cloudformation/ssm-relay/README.md @@ -0,0 +1,207 @@ +# SSM Relay Stack — Private VPC Port-Forwarding Demo + +Provisions a fully private VPC (no Internet Gateway, no NAT Gateway) with an EC2 relay box running a mock JFrog Artifactory server. Demonstrates how AWS Systems Manager (SSM) port-forwarding enables secure data-plane connectivity to private resources without any inbound network access. + +## Architecture + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Your Machine (Devin) │ +│ │ +│ AWS CLI + Session Manager Plugin │ +│ IAM User: ssm-relay-demo-ssm-portforward-user │ +│ (can ONLY port-forward to the specific relay instance) │ +│ │ +│ localhost:8081 ──► SSM Port-Forwarding Session │ +└──────────────┬──────────────────────────────────────────────┘ + │ (encrypted SSM channel over HTTPS) + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ AWS VPC (10.200.0.0/16) — NO Internet Gateway │ +│ │ +│ ┌─────────────────────┐ ┌──────────────────────────┐ │ +│ │ VPC Endpoints │ │ Private Subnet A │ │ +│ │ ├─ ssm │ │ ┌──────────────────────┐ │ │ +│ │ ├─ ssmmessages │◄──►│ │ EC2 Relay Box │ │ │ +│ │ ├─ ec2messages │ │ │ (Amazon Linux 2023) │ │ │ +│ │ └─ s3 (gateway) │ │ │ │ │ │ +│ └─────────────────────┘ │ │ nginx :8081 │ │ │ +│ │ │ Mock Artifactory │ │ │ +│ │ │ ├─ /api/system/ping │ │ │ +│ │ │ ├─ /api/system/ver. │ │ │ +│ │ │ └─ /api/repositories │ │ │ +│ │ └──────────────────────┘ │ │ +│ └──────────────────────────┘ │ +│ │ +│ Security Groups: │ +│ ├─ Relay SG: ingress only from VPC CIDR on port 8081 │ +│ └─ VPC Endpoint SG: HTTPS (443) from VPC CIDR │ +└─────────────────────────────────────────────────────────────┘ +``` + +## What This Demonstrates + +| Capability | How | +|---|---| +| **Private VPC with no ingress** | No IGW, no NAT, no public subnets — zero internet-initiated traffic | +| **SSM without internet** | VPC Interface Endpoints for `ssm`, `ssmmessages`, `ec2messages` | +| **Package installation in airgapped VPC** | S3 Gateway Endpoint enables `dnf` to reach Amazon Linux repos | +| **Fine-grained IAM** | Dedicated IAM user scoped to `ssm:StartSession` on one instance + one SSM document | +| **Data-plane connectivity** | SSM port-forwarding tunnels localhost traffic to private resources | +| **Mock Artifactory** | nginx serving realistic JFrog Artifactory API responses on port 8081 | + +## Resources Created + +| Resource | Type | Purpose | +|---|---|---| +| VPC | `AWS::EC2::VPC` | Isolated network (10.200.0.0/16) | +| Private Subnet A/B | `AWS::EC2::Subnet` | Two AZs, no public IPs | +| Route Table | `AWS::EC2::RouteTable` | Local routes only | +| S3 Gateway Endpoint | `AWS::EC2::VPCEndpoint` | Package manager access (dnf/yum) | +| SSM Interface Endpoints (x3) | `AWS::EC2::VPCEndpoint` | SSM agent registration + sessions | +| VPC Endpoint Security Group | `AWS::EC2::SecurityGroup` | HTTPS from VPC CIDR | +| Relay Security Group | `AWS::EC2::SecurityGroup` | Port 8081 from VPC CIDR only | +| EC2 Instance Role | `AWS::IAM::Role` | `AmazonSSMManagedInstanceCore` | +| EC2 Instance Profile | `AWS::IAM::InstanceProfile` | Attached to relay box | +| EC2 Relay Instance | `AWS::EC2::Instance` | Amazon Linux 2023 + nginx | +| IAM Port-Forward User | `AWS::IAM::User` | Fine-grained SSM-only permissions | +| IAM Policy | `AWS::IAM::Policy` | Scoped to specific instance + document | +| IAM Access Key | `AWS::IAM::AccessKey` | Credentials for the port-forward user | + +## Quick Start + +### Prerequisites + +- AWS CLI v2 +- [Session Manager Plugin](https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html) +- An AWS account with permissions to create CloudFormation stacks with IAM resources + +### Deploy + +```bash +aws cloudformation create-stack \ + --stack-name ssm-relay-demo \ + --template-body file://template.yaml \ + --capabilities CAPABILITY_NAMED_IAM \ + --region us-east-1 +``` + +### Get Outputs + +```bash +aws cloudformation describe-stacks \ + --stack-name ssm-relay-demo \ + --query 'Stacks[0].Outputs' \ + --output table +``` + +### Connect + +```bash +# Export the fine-grained user credentials from stack outputs +export AWS_ACCESS_KEY_ID= +export AWS_SECRET_ACCESS_KEY= +export AWS_DEFAULT_REGION=us-east-1 + +# Establish SSM port-forwarding tunnel +aws ssm start-session \ + --target \ + --document-name AWS-StartPortForwardingSession \ + --parameters '{"portNumber":["8081"],"localPortNumber":["8081"]}' +``` + +### Verify Connectivity + +In a separate terminal: + +```bash +# Health check +curl -s http://localhost:8081/artifactory/api/system/ping +# → OK + +# Version info +curl -s http://localhost:8081/artifactory/api/system/version | jq +# → {"version":"7.77.0","revision":"mock-ssm-relay-demo",...} + +# Repository list +curl -s http://localhost:8081/artifactory/api/repositories | jq +# → [{...libs-release-local...}, {...npm-remote...}, {...docker-local...}] +``` + +### Teardown + +```bash +aws cloudformation delete-stack --stack-name ssm-relay-demo --region us-east-1 +``` + +## IAM Policy Details + +The `ssm-relay-demo-ssm-portforward-user` has the minimum permissions needed: + +```json +{ + "Statement": [ + { + "Sid": "AllowStartSessionOnInstance", + "Effect": "Allow", + "Action": ["ssm:StartSession"], + "Resource": ["arn:aws:ec2:REGION:ACCOUNT:instance/INSTANCE_ID"], + "Condition": { + "BoolIfExists": { + "ssm:SessionDocumentAccessCheck": "true" + } + } + }, + { + "Sid": "AllowPortForwardDocumentOnly", + "Effect": "Allow", + "Action": ["ssm:StartSession"], + "Resource": ["arn:aws:ssm:REGION::document/AWS-StartPortForwardingSession"] + }, + { + "Sid": "AllowTerminateOwnSession", + "Effect": "Allow", + "Action": ["ssm:TerminateSession", "ssm:ResumeSession"], + "Resource": ["arn:aws:ssm:REGION:ACCOUNT:session/${aws:username}-*"] + }, + { + "Sid": "AllowDescribeInstances", + "Effect": "Allow", + "Action": [ + "ssm:DescribeInstanceInformation", + "ssm:GetConnectionStatus", + "ssm:DescribeSessions", + "ec2:DescribeInstances" + ], + "Resource": "*" + } + ] +} +``` + +This user **cannot**: +- Start interactive shell sessions (only port-forwarding) +- Target any other instance +- Use any other SSM document +- Terminate other users' sessions + +## Devin Environment Integration + +To use this as part of Devin's environment initialization, add to the `initialize` section: + +```bash +# Install SSM Session Manager Plugin +curl -s "https://s3.amazonaws.com/session-manager-downloads/plugin/latest/ubuntu_64bit/session-manager-plugin.deb" \ + -o /tmp/session-manager-plugin.deb && sudo dpkg -i /tmp/session-manager-plugin.deb +``` + +And to the `maintenance` section (requires SSM credentials as Devin secrets): + +```bash +# Establish SSM tunnel to private Artifactory +aws ssm start-session \ + --target \ + --document-name AWS-StartPortForwardingSession \ + --parameters '{"portNumber":["8081"],"localPortNumber":["8081"]}' \ + --region us-east-1 & +``` diff --git a/cloudformation/ssm-relay/template.yaml b/cloudformation/ssm-relay/template.yaml new file mode 100644 index 0000000..d57e854 --- /dev/null +++ b/cloudformation/ssm-relay/template.yaml @@ -0,0 +1,458 @@ +AWSTemplateFormatVersion: "2010-09-09" +Description: > + SSM Relay Stack — Provisions a private VPC with no internet ingress, + an EC2 relay box running a mock Artifactory (nginx), VPC Endpoints for + AWS Systems Manager, and a fine-grained IAM user that can only establish + SSM port-forwarding sessions. Demonstrates secure data-plane connectivity + to private resources without any inbound network access. + +# --------------------------------------------------------------------------- +# Parameters +# --------------------------------------------------------------------------- +Parameters: + EnvironmentName: + Type: String + Default: ssm-relay-demo + Description: Prefix applied to all resource names for easy identification. + + VpcCidr: + Type: String + Default: 10.200.0.0/16 + Description: CIDR block for the VPC. + + PrivateSubnetACidr: + Type: String + Default: 10.200.1.0/24 + Description: CIDR block for private subnet in AZ-a. + + PrivateSubnetBCidr: + Type: String + Default: 10.200.2.0/24 + Description: CIDR block for private subnet in AZ-b. + + InstanceType: + Type: String + Default: t3.micro + Description: EC2 instance type for the relay box. + + LatestAmiId: + Type: AWS::SSM::Parameter::Value + Default: /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64 + Description: > + SSM Parameter path that resolves to the latest Amazon Linux 2023 AMI. + The SSM agent is pre-installed on this AMI. + + MockArtifactoryPort: + Type: Number + Default: 8081 + Description: Port the mock Artifactory (nginx) listens on inside the relay box. + +# --------------------------------------------------------------------------- +# Resources +# --------------------------------------------------------------------------- +Resources: + + # ========================================================================= + # 1. VPC — completely private, NO Internet Gateway + # ========================================================================= + Vpc: + Type: AWS::EC2::VPC + Properties: + CidrBlock: !Ref VpcCidr + EnableDnsSupport: true + EnableDnsHostnames: true + Tags: + - Key: Name + Value: !Sub "${EnvironmentName}-vpc" + + # Two private subnets across AZs for resilience + PrivateSubnetA: + Type: AWS::EC2::Subnet + Properties: + VpcId: !Ref Vpc + CidrBlock: !Ref PrivateSubnetACidr + AvailabilityZone: !Select [0, !GetAZs ""] + MapPublicIpOnLaunch: false + Tags: + - Key: Name + Value: !Sub "${EnvironmentName}-private-a" + + PrivateSubnetB: + Type: AWS::EC2::Subnet + Properties: + VpcId: !Ref Vpc + CidrBlock: !Ref PrivateSubnetBCidr + AvailabilityZone: !Select [1, !GetAZs ""] + MapPublicIpOnLaunch: false + Tags: + - Key: Name + Value: !Sub "${EnvironmentName}-private-b" + + # Route table — local routes only (no NAT, no IGW) + PrivateRouteTable: + Type: AWS::EC2::RouteTable + Properties: + VpcId: !Ref Vpc + Tags: + - Key: Name + Value: !Sub "${EnvironmentName}-private-rt" + + PrivateSubnetARouteAssoc: + Type: AWS::EC2::SubnetRouteTableAssociation + Properties: + SubnetId: !Ref PrivateSubnetA + RouteTableId: !Ref PrivateRouteTable + + PrivateSubnetBRouteAssoc: + Type: AWS::EC2::SubnetRouteTableAssociation + Properties: + SubnetId: !Ref PrivateSubnetB + RouteTableId: !Ref PrivateRouteTable + + # ========================================================================= + # 2. Security Groups + # ========================================================================= + + # SG for VPC Endpoints — allows HTTPS from within the VPC + VpcEndpointSecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: Allow HTTPS traffic from VPC CIDR to SSM VPC Endpoints + VpcId: !Ref Vpc + SecurityGroupIngress: + - IpProtocol: tcp + FromPort: 443 + ToPort: 443 + CidrIp: !Ref VpcCidr + Description: HTTPS from VPC CIDR for SSM endpoints + Tags: + - Key: Name + Value: !Sub "${EnvironmentName}-vpce-sg" + + # SG for the Relay EC2 — NO ingress from internet; only self-referencing + # for mock artifactory health checks and SSM-tunneled traffic + RelaySecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: Relay box SG - no internet ingress - internal mock artifactory only + VpcId: !Ref Vpc + SecurityGroupIngress: + - IpProtocol: tcp + FromPort: !Ref MockArtifactoryPort + ToPort: !Ref MockArtifactoryPort + CidrIp: !Ref VpcCidr + Description: Mock Artifactory from within VPC only + Tags: + - Key: Name + Value: !Sub "${EnvironmentName}-relay-sg" + + # ========================================================================= + # 3a. S3 Gateway Endpoint — required so the instance can install packages + # via dnf/yum (Amazon Linux repos are hosted on S3). + # ========================================================================= + S3GatewayEndpoint: + Type: AWS::EC2::VPCEndpoint + Properties: + VpcId: !Ref Vpc + ServiceName: !Sub "com.amazonaws.${AWS::Region}.s3" + VpcEndpointType: Gateway + RouteTableIds: + - !Ref PrivateRouteTable + + # ========================================================================= + # 3b. VPC Endpoints for SSM (Interface Endpoints) + # These allow the EC2 instance to register with SSM without internet. + # ========================================================================= + SsmEndpoint: + Type: AWS::EC2::VPCEndpoint + Properties: + VpcId: !Ref Vpc + ServiceName: !Sub "com.amazonaws.${AWS::Region}.ssm" + VpcEndpointType: Interface + PrivateDnsEnabled: true + SubnetIds: + - !Ref PrivateSubnetA + - !Ref PrivateSubnetB + SecurityGroupIds: + - !Ref VpcEndpointSecurityGroup + + SsmMessagesEndpoint: + Type: AWS::EC2::VPCEndpoint + Properties: + VpcId: !Ref Vpc + ServiceName: !Sub "com.amazonaws.${AWS::Region}.ssmmessages" + VpcEndpointType: Interface + PrivateDnsEnabled: true + SubnetIds: + - !Ref PrivateSubnetA + - !Ref PrivateSubnetB + SecurityGroupIds: + - !Ref VpcEndpointSecurityGroup + + Ec2MessagesEndpoint: + Type: AWS::EC2::VPCEndpoint + Properties: + VpcId: !Ref Vpc + ServiceName: !Sub "com.amazonaws.${AWS::Region}.ec2messages" + VpcEndpointType: Interface + PrivateDnsEnabled: true + SubnetIds: + - !Ref PrivateSubnetA + - !Ref PrivateSubnetB + SecurityGroupIds: + - !Ref VpcEndpointSecurityGroup + + # ========================================================================= + # 4. IAM Role + Instance Profile for the EC2 relay box + # Grants SSM managed-instance permissions so the agent can register. + # ========================================================================= + RelayInstanceRole: + Type: AWS::IAM::Role + Properties: + RoleName: !Sub "${EnvironmentName}-relay-instance-role" + AssumeRolePolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Principal: + Service: ec2.amazonaws.com + Action: sts:AssumeRole + ManagedPolicyArns: + - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore + Tags: + - Key: Name + Value: !Sub "${EnvironmentName}-relay-instance-role" + + RelayInstanceProfile: + Type: AWS::IAM::InstanceProfile + Properties: + InstanceProfileName: !Sub "${EnvironmentName}-relay-instance-profile" + Roles: + - !Ref RelayInstanceRole + + # ========================================================================= + # 5. EC2 Relay Box — runs mock Artifactory (nginx) on a private subnet + # ========================================================================= + RelayInstance: + Type: AWS::EC2::Instance + DependsOn: + - S3GatewayEndpoint + - SsmEndpoint + - SsmMessagesEndpoint + - Ec2MessagesEndpoint + Properties: + InstanceType: !Ref InstanceType + ImageId: !Ref LatestAmiId + IamInstanceProfile: !Ref RelayInstanceProfile + SubnetId: !Ref PrivateSubnetA + SecurityGroupIds: + - !Ref RelaySecurityGroup + Tags: + - Key: Name + Value: !Sub "${EnvironmentName}-relay-box" + - Key: Purpose + Value: SSM relay with mock Artifactory + UserData: + Fn::Base64: !Sub | + #!/bin/bash + set -euxo pipefail + + # --------------------------------------------------------------- + # Install and configure nginx as a mock JFrog Artifactory server + # --------------------------------------------------------------- + dnf install -y nginx + + # Create mock Artifactory API response files + mkdir -p /usr/share/nginx/html/artifactory/api/system + + echo 'OK' > /usr/share/nginx/html/artifactory/api/system/ping + + cat > /usr/share/nginx/html/artifactory/api/system/version <<'EOF' + {"version":"7.77.0","revision":"mock-ssm-relay-demo","license":"Enterprise","addons":["docker","npm","pypi","maven"]} + EOF + + cat > /usr/share/nginx/html/artifactory/api/repositories <<'EOF' + [{"key":"libs-release-local","type":"LOCAL","packageType":"maven"},{"key":"npm-remote","type":"REMOTE","packageType":"npm"},{"key":"docker-local","type":"LOCAL","packageType":"docker"}] + EOF + + cat > /usr/share/nginx/html/index.html <<'EOF' + Mock Artifactory - SSM Relay Demo +

Mock JFrog Artifactory

+

Private instance inside VPC with no internet ingress. Reached via AWS SSM port-forwarding.

+ + EOF + + # Configure nginx to listen on the mock artifactory port + cat > /etc/nginx/conf.d/mock-artifactory.conf < + Command to establish an SSM port-forwarding session from your local + machine to the private mock Artifactory. + Value: !Sub > + aws ssm start-session + --target ${RelayInstance} + --document-name AWS-StartPortForwardingSession + --parameters '{"portNumber":["${MockArtifactoryPort}"],"localPortNumber":["8081"]}' + --region ${AWS::Region} + + ConnectivityTestCommand: + Description: > + After establishing the SSM tunnel, run this to verify connectivity + to the private mock Artifactory. + Value: "curl -s http://localhost:8081/artifactory/api/system/ping" + + ArchitectureSummary: + Description: What this stack demonstrates + Value: > + Private VPC (no IGW) -> EC2 relay with SSM agent -> Mock Artifactory (nginx:8081). + Access via SSM port-forwarding only. IAM user scoped to port-forward sessions + targeting this specific instance. No inbound internet traffic possible.