Skip to content

Stop the podman suite tripping sshd's per-source penalty - #301

Merged
karoltheguy merged 4 commits into
mainfrom
fix/podman-suite-ssh-penalty
Aug 3, 2026
Merged

Stop the podman suite tripping sshd's per-source penalty#301
karoltheguy merged 4 commits into
mainfrom
fix/podman-suite-ssh-penalty

Conversation

@karoltheguy

Copy link
Copy Markdown
Owner

The failure

The GitHub / podman job has been failing on the last two or three tests of an
otherwise green run, in asyncssh with ConnectionLost. It looked PR-specific,
because each branch lost a different tail test and the branches bumped
unrelated things, but PR #295, PR #297 and main (run 30828236096) all failed
the same way within twenty minutes of each other.

The sshd journal from inside the host container names the cause outright:

sshd[189]: drop connection #0 from [172.18.0.1]:44264 on [172.18.0.3]:22
           penalty: connections without attempting authentication

Why

OpenSSH 9.8 added PerSourcePenalties and Fedora ships it enabled. sshd charges
a source address one penalty second per TCP connection that closes without
attempting authentication, and starts dropping connections outright once the
accrued total passes the default 15s floor:

persourcepenalties crash:90 authfail:5 noauth:1 grace-exceeded:10
                   refuseconnection:10 max:600 min:15 ...

podman_target probes the port with a bare socket.create_connection, so a
developer with no host up gets a skip rather than a wall of red. At function
scope that ran once per test. Every client sits behind the compose bridge and so
shares one source address, 172.18.0.1, which makes the whole run a single
offender: 34 unauthenticated connections in 45 seconds, comfortably past the
floor, and the tests still running when it tripped are the ones that died.

That also explains the shape of it. Always the tail of the run, never the start;
a different test each time, because the crossing point moves with timing; and
intermittent on main, where a fast run squeaked under the threshold.

The fix

Two changes, both wanted.

  • tests/podman/conftest.py: podman_target becomes session-scoped, cutting
    the probes from roughly forty per run to one. That is all the probe was ever
    meant to be.
  • Dockerfile.podman-host: PerSourcePenalties no in the sshd drop-in, because
    a rate limiter that a full run can still reach is one that will fire again the
    next time the suite grows.

The drop-in already verifies its own effect through sshd -G, so the new
directive is asserted the same way. sshd -G exits non-zero on an unknown
keyword, so a base image bump that drops the option fails the build rather than
quietly restoring the flake.

The app itself is untouched. It authenticates every connection it opens and so
never accrued these penalties; only the test probe did. No production sshd
config changes.

Verification

Built against the real base image (fedora:43 + openssh-server):

usepam yes
passwordauthentication no
kbdinteractiveauthentication no
persourcepenalties no
ASSERTION PASSED

Removing the drop-in in the same container confirms the default was
noauth:1 against min:15.

Full suite against the real host: 36 passed, 4 skipped, 1102 deselected in
35.13s
. The 4 skips are the tests/e2e/test_podman_e2e.py ones, which need
the app container.

Follow-up

#295 and #297 will not go green on their own. Once this lands they each need a
rebase (@dependabot rebase) to pick it up.

The 303 that _get_session raises for a cookieless request was invisible
in the OpenAPI schema, so the generated docs claimed 44 endpoints could
never redirect. Static analysis flagged roughly 36 of them.

Adds AUTH_REDIRECT_RESPONSES and composes the two existing response
dicts off it, so the four routes that already documented 400 and 403
inherit the 303 without restating its wording.

Two tests carry the contract, because either one alone is insufficient.
The sweep walks each route's dependency graph, the same graph that
causes the 303, and asserts the annotation is present; deriving the
route set from the cause rather than a hand-written list is what makes
a future route added without the annotation fail. That only proves the
annotation exists, not that it is true, so a second test pins the
constant to the status _get_session actually raises: switching the
raise to 401 now fails loudly instead of leaving 44 stale annotations
green.

The two websocket routes are left alone; responses= has no meaning for
a handshake.
Ruff flagged UP006 and UP007 on the new AUTH_REDIRECT_RESPONSES. The
two dicts below it carry the same legacy typing style and were only
unreported because they are outside this diff, so all three move
together; leaving one modern constant wedged between two old ones
would be worse than either style applied consistently.

Dict and Union are now unused and drop out of the typing import.
Optional stays: the four remaining Optional annotations are untouched
Form defaults on unrelated route signatures, and converting them here
would be churn beyond this change.
The `podman` job failed on the last two or three tests of an otherwise green
run, in asyncssh with `ConnectionLost`, on PR #295, PR #297 and on main. It
looked PR-specific because each branch lost a different tail test, but the
cause is shared and the sshd journal names it:

    sshd[189]: drop connection #0 from [172.18.0.1]:44264 on [172.18.0.3]:22
               penalty: connections without attempting authentication

OpenSSH 9.8 added PerSourcePenalties and Fedora ships it enabled. sshd charges
a source address one penalty second per TCP connection that closes without
attempting authentication, and starts dropping connections outright once the
accrued total passes the default 15s floor.

The suite generated 34 of those in 45 seconds. `podman_target` probes the port
with a bare `socket.create_connection`, to skip rather than fail when no host
is up, and at function scope that ran once per test. Every client sits behind
the compose bridge and so shares one source address, which makes the whole run
a single offender.

Two changes, both wanted. The probe becomes session-scoped, which is all it was
ever meant to be, and the test image turns the penalty off, because a rate
limiter a full run can still reach is one that will fire again the next time
the suite grows. The sshd drop-in already asserts its own effect through
`sshd -G`, so the new directive is verified the same way and a base image that
drops the option will fail the build rather than quietly restore the flake.

Verified on fedora:43 with openssh-server, where `sshd -G` reports
`persourcepenalties no` under the drop-in and `noauth:1 ... min:15` without it,
and against the real host: 36 passed, 4 skipped, in 35s.
@codacy-production

codacy-production Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@sonarqubecloud

sonarqubecloud Bot commented Aug 3, 2026

Copy link
Copy Markdown

@karoltheguy
karoltheguy merged commit 261b843 into main Aug 3, 2026
15 checks passed
@karoltheguy
karoltheguy deleted the fix/podman-suite-ssh-penalty branch August 3, 2026 16:53
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.

1 participant