Skip to content

Report the ship runner's source address, and fix the dual-stack guidance - #90

Merged
aterga merged 3 commits into
mainfrom
claude/ship-source-address-and-dualstack-docs
Jul 28, 2026
Merged

Report the ship runner's source address, and fix the dual-stack guidance#90
aterga merged 3 commits into
mainfrom
claude/ship-source-address-and-dualstack-docs

Conversation

@aterga

@aterga aterga commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Summary

Two corrections from bringing production up, both of which cost real deploys.

Revoking 22/tcp from 0.0.0.0/0 broke staging and nearly broke the production release. The reasoning was that sg-0cbc9aa49683725f0 already permits 22 from 10.0.0.0/8 and the ship job reaches the host on an RFC1918 address, so the world-facing rule looked redundant. It isn't: reaching an RFC1918 destination says nothing about the source, and the ship pods present something outside 10/8, so that rule was the only thing allowing them. Staging's deploy failed at ssh: connect to host *** port 22: Connection timed out, and the production release only survived because the rule went back before its ship job started.

The runner can't answer this for itself — curl ifconfig.me reports public internet egress, which this path doesn't use. So ask the host: the ship job now reads $SSH_CLIENT over the connection it just made and prints the address the host sees. Every run, non-fatally.

The dual-stack guidance was framed wrongly. It said to publish both an A and an AAAA record, which obscures which one matters. AAAA-only is the dangerous case — and both hosts sat in it unnoticed, because every observer had IPv6. A-only is fine: IPv4 reaches effectively everyone, and IPv6-only clients arrive via NAT64/DNS64. Production is deliberately A-only now, so the README contradicted reality as well as being misleading.

Related issues

Does not close the SSH question. This makes the missing input observable; narrowing the rule still needs the documented source ranges from whoever operates the runner pools.

Changes

  • deploy-native.yml: add a Report source address as seen by the host step to ship, between Configure SSH and Deploy. Reads $SSH_CLIENT over SSH and prints the source the host observes. Warns rather than failing if SSH is unavailable, since Deploy reports that properly a step later.
  • deploy/native/README.md: new Narrowing inbound SSH subsection explaining why the rule is still open, why 10.0.0.0/8 doesn't cover it, how to read the source from the host ($SSH_CLIENT, or journalctl -u sshd | grep 'Accepted publickey'), that one sample is insufficient because the pools are ephemeral pods across multiple clusters, and that the 10.0.0.0/8 group has no IPv6 entry.
  • deploy/native/README.md: rewrite the dual-stack paragraph. Keeps the AAAA-only warning, notes it actually happened, states that A-only is acceptable and why, and says to verify IPv4 from a network without v6 rather than from a VPN or cloud shell that would mask a dead v4 path.
  • Drop "on-prem" from the build/ship split description — both environments ship from the shared dind-small pool, not an on-prem runner.

Testing

  • deploy-native.yml parses as YAML; ship's step order verified as Checkout → Download binary → Restore executable bit → Configure SSH → Report source address → Deploy → Verify deployed version.
  • The new step's shell logic exercised against a stubbed ssh for all three paths: reachable host prints the address, ssh failing exits non-zero and takes the warning branch, and an empty $SSH_CLIENT also takes the warning branch. The job continues in every case.
  • Not exercised end to end — this runs only on a push to main or a release tag. The staging deploy that follows this merge is what exercises it, and its output is the value needed for Put mcp.internetcomputer.org behind Cloudflare: origin cert plus an SSE compatibility test #89's sibling work on the SSH rule.
  • Not run: cargo build / cargo test / clippy — no Rust source changed.
  • Not run: npm test --prefix monitoring/mcp-status — no dashboard code changed; the workflow runs it anyway.

Checklist

  • I have read the Contributing guidelines.
  • Docs (README / comments) updated for any user-visible change.
  • No secrets, credentials, or internal-only information are included.

Note

The step costs one extra SSH round trip per deploy and prints a private address into the run log. That address is already visible in the same log via the arch-guard output, so it discloses nothing new — but it is worth knowing that is where the answer will appear.


Generated by Claude Code

Two corrections drawn from bringing production up, both of which cost real
deploys.

Revoking 22/tcp from 0.0.0.0/0 broke staging and nearly broke the production
release. The reasoning was that the security group already permits 22 from
10.0.0.0/8 and the ship job reaches the host on an RFC1918 address, so the
world-facing rule was redundant. Reaching an RFC1918 destination says nothing
about the source: the pods present something outside 10/8, so that rule was the
only thing allowing them. The runner cannot answer this for itself either, since
`curl ifconfig.me` reports public internet egress and this path does not use it.

So ask the host. The ship job now reads $SSH_CLIENT over the connection it just
made and prints the address the host sees, every run, non-fatally. Whoever
narrows the rule needs that value, and needs more than one sample of it: the
pools are ephemeral pods across more than one cluster, so consecutive deploys can
differ. The README says so, and records that the group permitting 22 from
10.0.0.0/8 carries no IPv6 entry.

Separately, the dual-stack section said to publish both an A and an AAAA record.
The families are not symmetric and that framing hid which one matters. AAAA-only
is the dangerous case, and both hosts were in it unnoticed because every observer
had IPv6. A-only is fine — IPv4 reaches effectively everyone and IPv6-only
clients arrive via NAT64/DNS64 — and production is deliberately A-only now. Say
that, and say to verify IPv4 from a network without v6 rather than from a VPN or
a cloud shell that would mask a dead v4 path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R8ZshwKmjD5fZ4Hs9dS6zh

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves deploy operability by making the ship runner’s actual SSH source address observable (as seen by the target host) and correcting the documentation guidance around dual-stack DNS reachability and runner/SSH assumptions.

Changes:

  • Adds a non-fatal “Report source address as seen by the host” SSH step in the ship job to print $SSH_CLIENT as observed by the host.
  • Updates deploy/native/README.md to clarify that IPv4 reachability is the critical requirement (warning on AAAA-only), and adds guidance on narrowing inbound SSH by measuring real source addresses.
  • Updates the build/ship split wording to remove “on-prem” framing and reflect the current runner model.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
.github/workflows/deploy-native.yml Adds a diagnostic SSH step to print the runner’s source address as seen by the host during deploy.
deploy/native/README.md Updates dual-stack guidance and documents how/why inbound SSH narrowing still requires observed runner source ranges.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread deploy/native/README.md Outdated
Comment thread .github/workflows/deploy-native.yml
claude added 2 commits July 28, 2026 15:10
The diagnostic step discarded ssh's stderr, which defeated much of its
purpose: "Connection timed out", "Permission denied (publickey)" and a host
key mismatch are entirely different diagnoses, and a step whose whole job is
to explain SSH problems should say which one happened. Capture stderr and
fold it into the annotation, collapsed to one line since ::warning:: is
line-oriented. Verified against stubs for all four outcomes.

The dual-stack paragraph asserted that IPv6-only clients reach IPv4-only
services via NAT64/DNS64. That holds only where the client's access network
provides the translation — it is not a property this deployment supplies, and
a v6-only client without it cannot reach an A-only host at all. Since the
paragraph is arguing that A-only is acceptable, overstating the fallback
makes the case look stronger than it is. Attribute it to the client network
and keep recommending an AAAA wherever the v6 path genuinely works.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R8ZshwKmjD5fZ4Hs9dS6zh
The section told the reader to obtain the runners' source ranges and narrow
the rule to them. That goal turns out not to exist. Calico source-NATs each
pod to its worker node's IP, and traffic to this VPC leaves over a routed
IPsec tunnel rather than a NATting gateway, so the node address arrives
unchanged. Node IPs are provisioned dynamically rather than declared, the two
clusters are separate sites with separate ranges and either can run the job,
and the SNAT-to-node-IP behaviour is an unpinned Calico default rather than a
contract. A prefix that fits today can stop fitting after cluster
maintenance, and it would surface as a deploy timeout.

So say that, and point at the fix that removes the requirement instead of
narrowing it: reach the host over SSM Session Manager, which needs no inbound
port. ssh works over it through a ProxyCommand, so deploy.sh is unaffected and
only the workflow's SSH setup changes. Also name the two circular
alternatives, since both look reasonable until you notice a job cannot
discover the address it would need to allow, and a bastion inherits the same
problem.

Record that the source is outside 10.0.0.0/8 as an established fact rather
than a guess: both hosts carry two independent rules permitting 22 from that
range, and removing only the 0.0.0.0/0 rule still broke the deploy.

Also note the ::/0 half of the rule is now gone, since the tunnels are
IPv4-only and it permitted nothing but internet-sourced attempts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R8ZshwKmjD5fZ4Hs9dS6zh
@aterga
aterga marked this pull request as ready for review July 28, 2026 15:41
@aterga
aterga requested a review from a team as a code owner July 28, 2026 15:41
Copilot AI review requested due to automatic review settings July 28, 2026 15:41
@aterga
aterga merged commit 3003942 into main Jul 28, 2026
2 checks passed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

.github/workflows/deploy-native.yml:213

  • The ::warning::... workflow command includes raw ssh stderr. GitHub Actions command parsing requires escaping % (and CR/LF) in command data; otherwise percent sequences like %0A can be interpreted/decoded by the runner, which can lead to workflow-command injection or malformed annotations. Since this content comes from the network, it should be sanitized before emitting the ::warning:: line.
          else
            echo "::warning::could not read SSH_CLIENT from the host: $(tr '\n' ' ' < "$err" 2>/dev/null)"
          fi

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants