Reach the AWS nodes by tunnelling SSH through Session Manager - #92
Closed
sethbergman wants to merge 1 commit into
Closed
sethbergman wants to merge 1 commit into
sethbergman wants to merge 1 commit into
Conversation
The nodes sit in private subnets with no public address and security.tf opens 22 from nowhere, so there was nothing outside the VPC to SSH to. docs/deployment.md said reaching them "needs SSM, a bastion, or a VPN" and the repository shipped none of the three: ansible.cfg is two lines and sets no connection arguments. The documented apply sequence ends in `cd ansible && ansible-playbook`, which could not have worked. Session Manager is the option that changes neither fact about the network. The agent holds an outbound connection to the SSM service and AWS-StartSSHSession carries an ordinary SSH session down it, so there is still no inbound rule and no public address, and the session is recorded against the caller's IAM identity. It also needs no new Terraform: iam.tf already attaches AmazonSSMManagedInstanceCore. ansible_host becomes the instance id, because --target names an instance to SSM and ProxyCommand's %h is whatever ansible_host holds. The private IP is not routable from where the playbook runs, so it would only look correct. ansible_user is set at all for the first time -- nothing defined it, so Ansible was connecting as the local username. Host keys are accept-new rather than no. A host here is an instance id, so a replaced node is a new name with a new key and is correctly unknown, while a changed key under an id already seen is the case worth refusing. It is still trust-on-first-use, and deployment.md says what verifying properly would take rather than implying this is it. ssh_key_name's description claimed that leaving it empty "disables SSH entirely and uses SSM Session Manager instead". That read as a supported arrangement and was not one: SSM replaces the network path, not the authentication, and what answers at the far end is still sshd reading authorized_keys. Corrected rather than made required, because `aws ssm start-session` with no document does give a shell, which is the one case where empty is deliberate. It is not enough for Ansible. Testing it needed a new shape. compose values are Jinja expressions, and a literal written bare is an undefined variable that composes to nothing rather than erroring -- so Ansible silently drops the setting, and the file is valid YAML either way, which is all tests/ansible checked. eval-compose.py renders them as aws_ec2 would and the suite asserts the results. Four mutations in tests/ansible/README.md, each watched to fail. One is worth repeating here. Under the unquoted-literal mutation the exclusion assertion -- that nothing throws the host-key check away -- stayed green, because a render that raised left no string to search. Its paired positive assertion went red, and that pairing is the only reason the mutation was caught. preflight-cloud.sh now checks for session-manager-plugin, since the AWS CLI execs it and without it every connection fails naming the plugin rather than the thing you were doing. A warning, not a failure: running the playbook from inside the VPC is legitimate and needs none of this. Three mutations on that, including removing the --cloud gate, which Azure's assertion catches. Azure is deliberately untouched. Its nodes are equally private, but adding an untested tunnel to a profile that has never been applied would make the gap harder to see rather than smaller. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #91 — it depends on
ansible_hostbeing the instance id.Base retargets to
mainwhen that merges.Second of the three blockers found while staging the first real AWS
apply.
The gap
The nodes sit in private subnets with no public address, and
security.tfopens port 22 from nowhere. There was nothing outside theVPC to SSH to.
docs/deployment.mdsaid reaching them "needs SSM, abastion, or a VPN" and the repository shipped none of the three —
ansible.cfgis two lines and sets no connection arguments. Thedocumented apply sequence ends in
cd ansible && ansible-playbook,which could not have worked.
Why Session Manager
It changes neither fact about the network: the agent holds an outbound
connection to the SSM service, and
AWS-StartSSHSessioncarries anordinary SSH session down it. No inbound rule, no public address, no
bastion to patch and pay for, and the session is recorded against the
caller's IAM identity rather than against whoever holds a key.
It also needs no new Terraform —
iam.tfalready attachesAmazonSSMManagedInstanceCore.Two consequences in the inventory:
ansible_hostis the instance id.--targetnames an instance toSSM, and
ProxyCommand's%his whateveransible_hostholds. Theprivate IP is not routable from where the playbook runs, so it would
only look correct.
ansible_useris set at all, for the first time. Nothing definedit, so Ansible was connecting as the local username.
Host keys are
accept-new, notno. A host here is an instance id, so areplaced node is a new name with a new key and is correctly unknown,
while a changed key under an id already seen is the case worth refusing.
It is still trust-on-first-use, and the doc says what verifying properly
would take rather than implying this is it.
ssh_key_namesaid something untrueIts description claimed that leaving it empty "disables SSH entirely and
uses SSM Session Manager instead". That reads as a supported arrangement
and was not one: SSM replaces the network path, not the authentication,
and what answers at the far end of the tunnel is still
sshdreadingauthorized_keys.Corrected rather than made required —
aws ssm start-sessionwith nodocument does give a shell, which is the one case where empty is
deliberate. It is not enough for Ansible, and the description now says
so.
Testing it needed a new shape
composevalues are Jinja expressions. A literal written bare —ansible_user: ec2-user— is an undefined variable, which composes tonothing rather than erroring: Ansible drops the setting silently, and
the file is valid YAML either way, which is all
tests/ansiblechecked.eval-compose.pyrenders them asaws_ec2would, against a syntheticinstance, and the suite asserts the results. Suite goes 71 → 80.
ansible_user: ec2-user, unquotedansible_host: private_ip_address--document-name AWS-StartSSHSessionStrictHostKeyChecking=noUserKnownHostsFile=/dev/nulladdedOne is worth repeating. Under the unquoted-literal mutation the
exclusion assertion — that nothing throws the host-key check away —
stayed green, because a render that raised left no string to search.
Its paired positive assertion went red, and that pairing is the only
reason the mutation was caught. An exclusion with nothing to exclude
passes.
Pre-flight
preflight-cloud.shnow checks forsession-manager-plugin. The AWS CLIexecs it, and without it every connection fails naming the plugin rather
than the thing you were doing. A warning, not a failure — running the
playbook from inside the VPC is legitimate and needs none of this, which
matches how the existing
ssh_key_namecheck is graded. Suite goes 43 →48, verified by removing the
--cloudgate (Azure's assertion catchesit) and by turning the warning into a failure.
What this does not do
Azure is deliberately untouched. Its nodes are equally private, but
adding an untested tunnel to a profile that has never been applied would
make the gap harder to see rather than smaller.
And none of this has run against a real account. It is configuration with
reasoning attached; the tests assert the values it produces, not that the
tunnel opens.
🤖 Generated with Claude Code