Skip to content

infra: remote locked state, confirmed destroy, resolving SSRF guard - #273

Merged
kurok merged 1 commit into
mainfrom
infra/remote-state-and-ssrf
Sep 19, 2026
Merged

kurok merged 1 commit into
mainfrom
infra/remote-state-and-ssrf

Conversation

@kurok

@kurok kurok commented Sep 19, 2026

Copy link
Copy Markdown
Owner

Closes #256. Nothing here was applied — this is a PR, as the issue asks.

State lived in the Jenkins workspace

A workspace wipe or an agent change orphaned a VPC, NAT gateway, ECS services and a Cloud Map namespace that kept billing and that the next run could not destroy, because it no longer had the state describing them.

versions.tf declares an S3 backend now, deliberately empty: bucket, lock table and region come from -backend-config at init. The file carries no account-specific names, and a missing one fails the init rather than quietly writing state to disk again.

destroy -auto-approve ran unattended

Against state the run did not create, that destroys whatever the state happens to describe, and there is no undo. It waits on an input step first.

The SSRF guard matched regexes against the URL string

Every one of these got past it:

http://user@10.0.0.1/ the pattern looking for an address saw http://user
http://0.0.0.0/ · http://[::1]/ · http://2130706433/ not in the list of ranges
any DNS name resolving to 169.254.169.254 the check never resolved anything

The check now lives in infra/jenkins/validate_target_url.py: it parses the URL, refuses userinfo, resolves the host, and requires every address it resolves to — not just the first — to be global. ipaddress.is_global covers the whole special-purpose registry rather than the handful of ranges anyone remembers to write out.

Python rather than Groovy, for two reasons: it has unit tests there and the pipeline has none here; and resolving a hostname from Groovy needs script approval on a sandboxed controller, which is a thing people grant once and forget.

Tests

Seven on the validator with DNS stubbed — covering every bypass above, including a name answering with one public and one private address, which is the standard way past a check that looks at a single answer. Plus three asserting the backend block, the destroy gate and the validator wiring. All fail on the old files.

One correction to the issue

It lists "no disableConcurrentBuilds()". It is already there, at Jenkinsfile:96, so nothing was needed.

Acceptance

  • Remote locked state ✓
  • Destroy requires confirmation ✓
  • SSRF check resolves hosts ✓
  • ruff format/check clean, mypy clean ✓
  • uv run pytest — 1889 passed, coverage 95.67% ✓

Worth your judgement before merging

  • TF_STATE_BUCKET / TF_LOCK_TABLE default to pywrkr-terraform-state / pywrkr-terraform-locks. Those must exist, and the first init against the new backend will ask to migrate the existing local state. That is a one-off manual step on the controller, not something this PR can do.
  • The validator needs python3 on the agent. That is already assumed — generate_report.py runs there too.

@codecov

codecov Bot commented Sep 19, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ All tests successful. No failed tests found.

📢 Thoughts on this report? Let us know!

Three things in the pipeline that creates and destroys real infrastructure.

State lived in the Jenkins workspace. A workspace wipe or an agent change
orphaned a VPC, NAT gateway, ECS services and a Cloud Map namespace that kept
billing and that the next run could not destroy, because it no longer had the
state describing them. versions.tf declares an S3 backend now, deliberately
empty: the bucket, lock table and region come from -backend-config at init, so
the file carries no account-specific names and a missing one fails the init
rather than quietly writing state to disk again.

terraform destroy -auto-approve ran unattended. Against state the run did not
create that destroys whatever the state happens to describe, and there is no
undo, so it waits on an input step first.

The TARGET_URL guard matched regexes against the whole URL string. Every one
of these got past it: http://user@10.0.0.1/ (the pattern saw "http://user"),
http://0.0.0.0/, http://[::1]/, http://2130706433/, and any DNS name pointing
at 169.254.169.254. The check now lives in
infra/jenkins/validate_target_url.py: it parses the URL, refuses userinfo,
resolves the host, and requires every address it resolves to -- not just the
first -- to be global. ipaddress.is_global covers the whole special-purpose
registry rather than the handful of ranges anyone remembers to write out.

Python rather than Groovy for two reasons: it has unit tests there and the
pipeline has none here, and resolving a hostname from Groovy needs script
approval on a sandboxed controller, which is a thing people grant once and
forget.

Seven tests on the validator with DNS stubbed, covering every bypass above,
plus three asserting the pipeline and backend themselves. All fail on the old
files.

Note: the issue also asks for disableConcurrentBuilds(); it is already there,
at Jenkinsfile:96.

Nothing here was applied. This is a PR, as the issue asks.

Signed-off-by: kurok <22548029+kurok@users.noreply.github.com>
@kurok
kurok force-pushed the infra/remote-state-and-ssrf branch from f4ac5fe to 5009d2e Compare September 19, 2026 15:09
@kurok
kurok merged commit e2c1815 into main Sep 19, 2026
27 of 29 checks passed
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.

infra(sre): Terraform state is local to the Jenkins workspace while the pipeline runs destroy -auto-approve; SSRF guard is bypassable

1 participant