{"id":12655,"date":"2026-08-28T21:59:25","date_gmt":"2026-08-29T02:59:25","guid":{"rendered":"https:\/\/www.rushworth.us\/lisa\/?p=12655"},"modified":"2026-11-02T10:04:34","modified_gmt":"2026-11-02T15:04:34","slug":"spire-prove-the-configuration-rejects-abuse-negative-security-testing","status":"publish","type":"post","link":"https:\/\/www.rushworth.us\/lisa\/?p=12655","title":{"rendered":"SPIFFE: Prove the Configuration Rejects Abuse: Negative Security Testing"},"content":{"rendered":"<p>A security architecture should be judged by rejection behavior, not only by a successful demonstration. Each test below states a precondition, an enforcement point, and evidence to collect. The exact HTTP status or OAuth error depends on the failing stage and implementation; do not require a universal error code where the standards or product do not prescribe one.<\/p>\n<h2>Test environment and positive controls<\/h2>\n<p>Use isolated non-production identities and resources. Establish a successful standalone call and a successful Lisa\/XYZ delegated call first. Capture the issued token\u2019s approved claims without copying credentials into ordinary logs. Confirm the actual policy version and key material in use. Change one condition per test where possible, and retain request correlation so failures can be attributed to the intended control.<\/p>\n<p>Where a deployment does not implement a proposed control, mark the corresponding test as an identified gap or accepted risk, not a false pass. Sender-constraint tests require a genuinely supported profile. Task-authorization tests require the resource server to implement that policy.<\/p>\n<table>\n<tbody>\n<tr>\n<td width=\"166\"><strong>ID<\/strong><\/td>\n<td width=\"166\"><strong>Input or precondition<\/strong><\/td>\n<td width=\"166\"><strong>Enforcement point<\/strong><\/td>\n<td width=\"166\"><strong>Expected outcome\/evidence<\/strong><\/td>\n<\/tr>\n<tr>\n<td width=\"166\"><strong>01<\/strong><\/td>\n<td width=\"166\">Actor token from untrusted issuer<\/td>\n<td width=\"166\">STS actor validation<\/td>\n<td width=\"166\">Reject; log issuer-policy failure<\/td>\n<\/tr>\n<tr>\n<td width=\"166\"><strong>02<\/strong><\/td>\n<td width=\"166\">Actor JWT-SVID with wrong STS audience<\/td>\n<td width=\"166\">STS actor validation<\/td>\n<td width=\"166\">Reject; intended-recipient failure<\/td>\n<\/tr>\n<tr>\n<td width=\"166\"><strong>03<\/strong><\/td>\n<td width=\"166\">Human JWT substituted as actor evidence<\/td>\n<td width=\"166\">STS profile validation<\/td>\n<td width=\"166\">Reject; cross-profile failure<\/td>\n<\/tr>\n<tr>\n<td width=\"166\"><strong>04<\/strong><\/td>\n<td width=\"166\">Workload JWT substituted as human subject<\/td>\n<td width=\"166\">STS subject validation<\/td>\n<td width=\"166\">Reject; subject-profile failure<\/td>\n<\/tr>\n<tr>\n<td width=\"166\"><strong>05<\/strong><\/td>\n<td width=\"166\">Unsigned or disallowed-algorithm JWT<\/td>\n<td width=\"166\">STS\/API signature policy<\/td>\n<td width=\"166\">Reject; algorithm\/signature failure<\/td>\n<\/tr>\n<tr>\n<td width=\"166\"><strong>06<\/strong><\/td>\n<td width=\"166\">Token with attacker-controlled key URL<\/td>\n<td width=\"166\">Configured key-resolution boundary<\/td>\n<td width=\"166\">No outbound trust in supplied URL; reject invalid signature<\/td>\n<\/tr>\n<tr>\n<td width=\"166\"><strong>07<\/strong><\/td>\n<td width=\"166\">Expired input token<\/td>\n<td width=\"166\">STS time validation<\/td>\n<td width=\"166\">Reject; expiration failure<\/td>\n<\/tr>\n<tr>\n<td width=\"166\"><strong>08<\/strong><\/td>\n<td width=\"166\">Input not yet valid outside allowed skew<\/td>\n<td width=\"166\">STS time validation<\/td>\n<td width=\"166\">Reject; temporal-policy failure<\/td>\n<\/tr>\n<tr>\n<td width=\"166\"><strong>09<\/strong><\/td>\n<td width=\"166\">Output token presented to wrong API<\/td>\n<td width=\"166\">Resource audience validation<\/td>\n<td width=\"166\">Reject; audience mismatch<\/td>\n<\/tr>\n<tr>\n<td width=\"166\"><strong>10<\/strong><\/td>\n<td width=\"166\">Forged act claim on signed token<\/td>\n<td width=\"166\">Resource signature validation<\/td>\n<td width=\"166\">Reject; modified token<\/td>\n<\/tr>\n<tr>\n<td width=\"166\"><strong>11<\/strong><\/td>\n<td width=\"166\">Caller actor override differs from verified identity<\/td>\n<td width=\"166\">STS mapping\/policy<\/td>\n<td width=\"166\">Reject or ignore override under explicit policy; no forged attribution<\/td>\n<\/tr>\n<tr>\n<td width=\"166\"><strong>12<\/strong><\/td>\n<td width=\"166\">Client\/actor pairing forbidden by policy<\/td>\n<td width=\"166\">STS relationship policy<\/td>\n<td width=\"166\">Reject; pairing decision recorded<\/td>\n<\/tr>\n<tr>\n<td width=\"166\"><strong>13<\/strong><\/td>\n<td width=\"166\">XYZ paired with ineligible Lisa delegation<\/td>\n<td width=\"166\">STS approval\/pairing policy<\/td>\n<td width=\"166\">Reject; subject\/actor eligibility failure<\/td>\n<\/tr>\n<tr>\n<td width=\"166\"><strong>14<\/strong><\/td>\n<td width=\"166\">Unauthorized output audience requested<\/td>\n<td width=\"166\">STS target policy<\/td>\n<td width=\"166\">Reject; disallowed exchange edge<\/td>\n<\/tr>\n<tr>\n<td width=\"166\"><strong>15<\/strong><\/td>\n<td width=\"166\">Scopes exceed approved grant<\/td>\n<td width=\"166\">STS scope policy<\/td>\n<td width=\"166\">Reject or explicitly downscope; record granted set<\/td>\n<\/tr>\n<tr>\n<td width=\"166\"><strong>16<\/strong><\/td>\n<td width=\"166\">Missing actor on delegated-only API<\/td>\n<td width=\"166\">API delegated-profile policy<\/td>\n<td width=\"166\">Reject; required-actor failure<\/td>\n<\/tr>\n<tr>\n<td width=\"166\"><strong>17<\/strong><\/td>\n<td width=\"166\">Malformed nested actor structure<\/td>\n<td width=\"166\">STS\/API schema validation<\/td>\n<td width=\"166\">Reject; malformed identity context<\/td>\n<\/tr>\n<tr>\n<td width=\"166\"><strong>18<\/strong><\/td>\n<td width=\"166\">Nested actor has privileged identity<\/td>\n<td width=\"166\">API authorization engine<\/td>\n<td width=\"166\">No privilege union; deny operation not allowed by top-level context<\/td>\n<\/tr>\n<tr>\n<td width=\"166\"><strong>19<\/strong><\/td>\n<td width=\"166\">Valid token mutates wrong case<\/td>\n<td width=\"166\">API object authorization<\/td>\n<td width=\"166\">Reject; resource mismatch<\/td>\n<\/tr>\n<tr>\n<td width=\"166\"><strong>20<\/strong><\/td>\n<td width=\"166\">Valid token changes forbidden field<\/td>\n<td width=\"166\">API mutation authorization<\/td>\n<td width=\"166\">Reject; actual-body constraint failure<\/td>\n<\/tr>\n<tr>\n<td width=\"166\"><strong>21<\/strong><\/td>\n<td width=\"166\">Approval expired before exchange<\/td>\n<td width=\"166\">STS approval service<\/td>\n<td width=\"166\">Reject; stale grant<\/td>\n<\/tr>\n<tr>\n<td width=\"166\"><strong>22<\/strong><\/td>\n<td width=\"166\">Approval revoked after token issuance<\/td>\n<td width=\"166\">API current-grant control if configured<\/td>\n<td width=\"166\">Reject within measured distribution\/cache window<\/td>\n<\/tr>\n<tr>\n<td width=\"166\"><strong>23<\/strong><\/td>\n<td width=\"166\">One-time authorization reused<\/td>\n<td width=\"166\">API transactional grant control<\/td>\n<td width=\"166\">No second mutation; atomic use evidence<\/td>\n<\/tr>\n<tr>\n<td width=\"166\"><strong>24<\/strong><\/td>\n<td width=\"166\">Stolen bearer token presented elsewhere<\/td>\n<td width=\"166\">Resource authentication<\/td>\n<td width=\"166\">May succeed absent sender constraint; document residual risk<\/td>\n<\/tr>\n<tr>\n<td width=\"166\"><strong>25<\/strong><\/td>\n<td width=\"166\">Wrong certificate for bound token<\/td>\n<td width=\"166\">mTLS\/token-binding verifier<\/td>\n<td width=\"166\">Reject; certificate-thumbprint mismatch<\/td>\n<\/tr>\n<tr>\n<td width=\"166\"><strong>26<\/strong><\/td>\n<td width=\"166\">Wrong DPoP key or token hash<\/td>\n<td width=\"166\">DPoP-capable resource<\/td>\n<td width=\"166\">Reject; proof\/token binding failure<\/td>\n<\/tr>\n<tr>\n<td width=\"166\"><strong>27<\/strong><\/td>\n<td width=\"166\">Replayed DPoP proof<\/td>\n<td width=\"166\">DPoP replay\/freshness verifier<\/td>\n<td width=\"166\">Reject per deployed replay policy; proof evidence<\/td>\n<\/tr>\n<tr>\n<td width=\"166\"><strong>28<\/strong><\/td>\n<td width=\"166\">Unknown kid during discovery outage<\/td>\n<td width=\"166\">STS\/API key validation<\/td>\n<td width=\"166\">Reject unknown key; bounded refresh attempts<\/td>\n<\/tr>\n<tr>\n<td width=\"166\"><strong>29<\/strong><\/td>\n<td width=\"166\">Planned key rollover with old\/new tokens<\/td>\n<td width=\"166\">STS\/API verification caches<\/td>\n<td width=\"166\">Valid intended tokens pass during planned overlap<\/td>\n<\/tr>\n<tr>\n<td width=\"166\"><strong>30<\/strong><\/td>\n<td width=\"166\">Disabled actor uses cached valid JWT<\/td>\n<td width=\"166\">Resource deny\/revocation controls<\/td>\n<td width=\"166\">Measure rejection latency; no unsupported instant-revocation claim<\/td>\n<\/tr>\n<tr>\n<td width=\"166\"><strong>31<\/strong><\/td>\n<td width=\"166\">Workload does not match registration selectors<\/td>\n<td width=\"166\">SPIRE workload issuance<\/td>\n<td width=\"166\">No XYZ credential; attestation\/issuance evidence<\/td>\n<\/tr>\n<tr>\n<td width=\"166\"><strong>32<\/strong><\/td>\n<td width=\"166\">Unrelated pod chooses XYZ service account<\/td>\n<td width=\"166\">Admission\/RBAC plus SPIRE policy<\/td>\n<td width=\"166\">Blocked deployment or deny issuance; otherwise record privilege gap<\/td>\n<\/tr>\n<tr>\n<td width=\"166\"><strong>33<\/strong><\/td>\n<td width=\"166\">Unauthorized node attempts enrollment<\/td>\n<td width=\"166\">SPIRE node attestation<\/td>\n<td width=\"166\">No accepted eligible agent identity<\/td>\n<\/tr>\n<tr>\n<td width=\"166\"><strong>34<\/strong><\/td>\n<td width=\"166\">Queued job runs after delegation revoked<\/td>\n<td width=\"166\">Worker authorization boundary<\/td>\n<td width=\"166\">Reject execution; log job and approval IDs<\/td>\n<\/tr>\n<tr>\n<td width=\"166\"><strong>35<\/strong><\/td>\n<td width=\"166\">Tenant\/resource identifier substitution<\/td>\n<td width=\"166\">STS and resource tenant policy<\/td>\n<td width=\"166\">Reject cross-tenant access<\/td>\n<\/tr>\n<tr>\n<td width=\"166\"><strong>36<\/strong><\/td>\n<td width=\"166\">Policy\/STS outage triggers retry fallback<\/td>\n<td width=\"166\">Application\/worker failure path<\/td>\n<td width=\"166\">No anonymous or broader-authority fallback<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>&nbsp;<\/p>\n<h2>Acceptance criteria<\/h2>\n<ul>\n<li>Every failure occurs at a documented boundary and emits useful non-secret audit evidence.<\/li>\n<li>No test silently changes a delegated request into impersonation or anonymous access.<\/li>\n<li>No requested audience or scope becomes trusted merely because it was requested.<\/li>\n<li>Resource-level constraints are checked against the actual operation and canonical object.<\/li>\n<li>Revocation and outage tests report measured windows, not assumed guarantees.<\/li>\n<li>Concurrent one-time execution tests demonstrate atomic behavior.<\/li>\n<li>Known valid controls still pass after policy tightening and planned key rotation.<\/li>\n<\/ul>\n<h2>Adversarial execution and evidence<\/h2>\n<p>Repeat tests with concurrency, stale caches, alternate replicas, and retrying workers. Exercise non-happy-path serialization and identifier normalization. Confirm that every API instance uses the same intended policy version. Capture both denial and business-state evidence: a rejected response is insufficient if a mutation occurred before the rejection.<\/p>\n<p>Takeaway: a useful test report says where abuse was blocked, why, what residual exposure remains, and how quickly changes reach every enforcement point.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>A security architecture should be judged by rejection behavior, not only by a successful demonstration. Each test below states a precondition, an enforcement point, and evidence to collect. The exact HTTP status or OAuth error depends on the failing stage and implementation; do not require a universal error code where the standards or product do &hellip;<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[2243],"tags":[2246,2244,2216,2245],"class_list":["post-12655","post","type-post","status-publish","format-standard","hentry","category-spiffe","tag-agent-identity","tag-spiffe","tag-spire","tag-workload-identity"],"_links":{"self":[{"href":"https:\/\/www.rushworth.us\/lisa\/index.php?rest_route=\/wp\/v2\/posts\/12655","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.rushworth.us\/lisa\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.rushworth.us\/lisa\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.rushworth.us\/lisa\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.rushworth.us\/lisa\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=12655"}],"version-history":[{"count":2,"href":"https:\/\/www.rushworth.us\/lisa\/index.php?rest_route=\/wp\/v2\/posts\/12655\/revisions"}],"predecessor-version":[{"id":12659,"href":"https:\/\/www.rushworth.us\/lisa\/index.php?rest_route=\/wp\/v2\/posts\/12655\/revisions\/12659"}],"wp:attachment":[{"href":"https:\/\/www.rushworth.us\/lisa\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=12655"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.rushworth.us\/lisa\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=12655"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.rushworth.us\/lisa\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=12655"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}