Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <profile>
```

2. Confirm the target node is registered and online in SSM:

```bash
aws ssm describe-instance-information \
--profile <profile> --region <region> \
--filters "Key=InstanceIds,Values=<instance-id>" \
--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 <profile> --region <region> \
--target <management-node-instance-id> \
--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

Expand Down
3 changes: 3 additions & 0 deletions pulumi/stalwart/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,7 @@ def __init__(
profile_postboot_attachment,
profile_s3_attachment,
profile_logwrite_attachment,
profile_ssm_attachment,
profile,
) = stalwart_iam.iam(
self,
Expand All @@ -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(),
Expand Down Expand Up @@ -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,
Expand Down
20 changes: 18 additions & 2 deletions pulumi/stalwart/iam.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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
Expand Down Expand Up @@ -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 (
Expand All @@ -83,5 +98,6 @@ def iam(
profile_postboot_attachment,
profile_s3_attachment,
profile_logwrite_attachment,
profile_ssm_attachment,
profile,
)
4 changes: 3 additions & 1 deletion pulumi/stalwart_instance_user_data.sh.j2
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading