diff --git a/README.md b/README.md index f2d5025..27f082b 100644 --- a/README.md +++ b/README.md @@ -275,6 +275,78 @@ Now you have access to the admin panel by pointing a browser on your local machi with which to run TLS. As a result, the admin panel will not load over HTTPS. You will have to disable TLS on this by manually editing the service configuration on the node. There are more details on how to manage nodes below. +This works because the tunnel forwards the *node's own* loopback `8080` back to your machine over a connection you +already own (the SSH session), rather than routing new network traffic in from outside. The SSM-based method below +uses the same trick over a different transport, which is why neither of them needs the bastion's or the private load +balancer's security group opened up for your IP. + + +### Accessing the Management Dashboard via AWS SSO / SSM (No Bastion, No SSH Keys) + +As an alternative to the SSH tunnel above, you can reach the management dashboard through +[AWS Systems Manager Session Manager](https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager.html), +authenticating with your AWS SSO login instead of an SSH key or a bastion host. + +**Why this works without opening any security group:** the AWS-managed `AWS-StartPortForwardingSession` document +runs entirely on the target instance. It forwards a port on the instance's own loopback interface back to your +machine over the SSM agent's outbound WebSocket connection to the Systems Manager service. That traffic never enters +the VPC as routed network traffic, so the instance's inbound security group rules are never evaluated. No SG changes, +no inbound rule for any IP, ever — same principle as the SSH tunnel above, just over a different transport. + +This is **not** the same as `AWS-StartPortForwardingSessionToRemoteHost`, which proxies real, routed network traffic +through a jump host to a *different* target. That document *is* still subject to the target's security group. If you +try to relay through the bastion with `ToRemoteHost` pointed at a management node's private IP on port 8080, it will +fail with `Connection to destination port failed`, because the management port's security group only allows the +private "management" load balancer as a source, not the bastion. Always target the management node itself with +plain `AWS-StartPortForwardingSession`, not the bastion. + +#### Prerequisites + +- Your IAM role/user must have `ssm:StartSession` permission (granted via your AWS SSO permission set), and the + target instance's IAM role must have the `AmazonSSMManagedInstanceCore` managed policy attached (this is built + into the Stalwart node role by this project; see `pulumi/stalwart/iam.py`). +- The instance needs outbound access to the SSM/EC2Messages/SSMMessages endpoints, either via the Internet or VPC + endpoints. Stalwart nodes already have this via their NAT/egress path. +- The `session-manager-plugin` must be installed locally. Follow AWS's + [Session Manager plugin installation instructions](https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html) + for your OS, then verify the install: + + ```bash + session-manager-plugin + # The Session Manager plugin was installed successfully. + ``` + +#### Steps + +1. Confirm you're logged into the right AWS SSO identity: + + ```bash + aws sts get-caller-identity --profile + ``` + +2. Confirm the target node is registered and online in SSM: + + ```bash + aws ssm describe-instance-information \ + --profile --region \ + --filters "Key=InstanceIds,Values=" \ + --query 'InstanceInformationList[].{Id:InstanceId,Ping:PingStatus}' \ + --output table + ``` + +3. Start the port-forwarding session directly to the management node (no bastion involved): + + ```bash + aws ssm start-session \ + --profile --region \ + --target \ + --document-name AWS-StartPortForwardingSession \ + --parameters '{"portNumber":["8080"],"localPortNumber":["8080"]}' + ``` + +4. Browse to https://localhost:8080/. As with the SSH tunnel above, expect a self-signed certificate warning on a + freshly-bootstrapped cluster that hasn't yet been issued a real TLS certificate. + ### Bootstrapping a Stalwart Node diff --git a/pulumi/stalwart/__init__.py b/pulumi/stalwart/__init__.py index 7479fa4..b80d70c 100644 --- a/pulumi/stalwart/__init__.py +++ b/pulumi/stalwart/__init__.py @@ -333,6 +333,7 @@ def __init__( profile_postboot_attachment, profile_s3_attachment, profile_logwrite_attachment, + profile_ssm_attachment, profile, ) = stalwart_iam.iam( self, @@ -347,6 +348,7 @@ def __init__( subnet = nodes[node_id].pop('subnet', None) or self.private_subnets[idx % len(self.private_subnets)] depends_on = [ profile, + profile_ssm_attachment, redis_secret, s3_secret, *self.private_load_balancer_security_groups.values(), @@ -428,6 +430,7 @@ def __init__( 'node_profile_logwrite_attachment': profile_logwrite_attachment, 'node_profile_postboot_policy_attachment': profile_postboot_attachment, 'node_profile_s3_policy_attachment': profile_s3_attachment, + 'node_profile_ssm_policy_attachment': profile_ssm_attachment, 'node_sgs': self.node_sgs, 'private_lbs': private_lbs, 'private_lb_dns': private_lb_dns, diff --git a/pulumi/stalwart/iam.py b/pulumi/stalwart/iam.py index c1fb6e2..a2d927e 100644 --- a/pulumi/stalwart/iam.py +++ b/pulumi/stalwart/iam.py @@ -8,12 +8,21 @@ from tb_pulumi.constants import ASSUME_ROLE_POLICY +AMAZON_SSM_MANAGED_INSTANCE_CORE_ARN = 'arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore' + + def iam( self, log_group_arn: str, s3_policy: aws.iam.Policy, ) -> tuple[ - aws.iam.Policy, aws.iam.Role, aws.iam.RolePolicyAttachment, aws.iam.RolePolicyAttachment, aws.iam.InstanceProfile + aws.iam.Policy, + aws.iam.Role, + aws.iam.RolePolicyAttachment, + aws.iam.RolePolicyAttachment, + aws.iam.RolePolicyAttachment, + aws.iam.RolePolicyAttachment, + aws.iam.InstanceProfile, ]: """Build IAM resources needed by Stalwart. @@ -22,7 +31,7 @@ def iam( :return: Series of IAM resources for Stalwart. :rtype: tuple[ tb_pulumi.iam.UserWithAccessKey, aws.iam.Policy, aws.iam.Role, aws.iam.RolePolicyAttachment, - aws.iam.InstanceProfile ] + aws.iam.RolePolicyAttachment, aws.iam.RolePolicyAttachment, aws.iam.InstanceProfile ] """ # Build a policy which will grant the nodes access to their own configuration data @@ -75,6 +84,12 @@ def iam( policy_arn=log_group_arn, ) + profile_ssm_attachment = aws.iam.RolePolicyAttachment( + f'{self.name}-rpa-nodeprofile-ssm', + role=role.name, + policy_arn=AMAZON_SSM_MANAGED_INSTANCE_CORE_ARN, + ) + profile = aws.iam.InstanceProfile(f'{self.name}-ip-nodeprofile', name=f'{self.name}-nodeprofile', role=role.name) return ( @@ -83,5 +98,6 @@ def iam( profile_postboot_attachment, profile_s3_attachment, profile_logwrite_attachment, + profile_ssm_attachment, profile, ) diff --git a/pulumi/stalwart_instance_user_data.sh.j2 b/pulumi/stalwart_instance_user_data.sh.j2 index b65b6d4..6218006 100644 --- a/pulumi/stalwart_instance_user_data.sh.j2 +++ b/pulumi/stalwart_instance_user_data.sh.j2 @@ -23,7 +23,9 @@ enabled=1' > /etc/yum.repos.d/fluent-bit.repo # Update system, install dependencies dnf update -y -dnf install -y bzip2 docker fluent-bit python3.12 +dnf install -y amazon-ssm-agent bzip2 docker fluent-bit python3.12 + +systemctl enable amazon-ssm-agent --now # Delete the default fluent-bit config; we'll template a new one in Phase 2 rm -f /etc/fluent-bit/fluent-bit.conf