From c9992efaca0c02323461d724f48e4a3a01fe8e9f Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Wed, 30 Sep 2026 16:48:49 +0000 Subject: [PATCH] noise: cover SSH verdict replay ledger and removed check A replay must neither record auth for auto-approval nor, once the pair is no longer under a check, be answered from the consumed verdict. --- hscontrol/noise_test.go | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/hscontrol/noise_test.go b/hscontrol/noise_test.go index c5ab554d5..5d50a9cca 100644 --- a/hscontrol/noise_test.go +++ b/hscontrol/noise_test.go @@ -917,6 +917,36 @@ func TestSSHActionFollowUp_ConsumedVerdictNotReplayed(t *testing.T) { second := sshActionFromRecorder(t, f.followUp(t, authID)) replayID := requireSSHHold(t, second) assert.NotEqual(t, authID, replayID, "re-delegation must mint a new session") + + // A replay that recorded auth would feed auto-approval. + _, recorded := f.ns.headscale.state.GetLastSSHAuth(f.src, f.dst) + assert.Equal(t, tc.accept, recorded, "only an accepted verdict may record auth") + }) + } + + // With the check gone there is nothing to re-delegate, but the replay + // must still not be answered from the consumed verdict. + for _, tc := range sshVerdictCases { + t.Run(tc.name+"-check-removed", func(t *testing.T) { + t.Parallel() + + f := newSSHVerdictFixture(t) + authID, auth := f.mint(t) + auth.FinishAuth(tc.verdict) + + first := sshActionFromRecorder(t, f.followUp(t, authID)) + require.True(t, carriesSSHVerdict(first, tc.accept), + "first follow-up must carry the verdict, got %+v", first) + + _, err := f.ns.headscale.state.SetPolicy([]byte(`{}`)) + require.NoError(t, err) + + _, checkFound := f.ns.headscale.state.SSHCheckParams(f.src, f.dst) + require.False(t, checkFound, "test setup: pair must no longer be subject to a check") + + rec := f.followUp(t, authID) + assert.Equal(t, http.StatusBadRequest, rec.Code, + "replay without a check must be refused, body=%s", rec.Body.String()) }) } }