Skip to content

secret/pki: lock ACME order finalize to prevent concurrent double-issuance#32004

Closed
Har1sh-k wants to merge 1 commit into
hashicorp:mainfrom
Har1sh-k:fix/acme-finalize-per-order-lock
Closed

secret/pki: lock ACME order finalize to prevent concurrent double-issuance#32004
Har1sh-k wants to merge 1 commit into
hashicorp:mainfrom
Har1sh-k:fix/acme-finalize-per-order-lock

Conversation

@Har1sh-k

Copy link
Copy Markdown
Contributor

Description

acmeFinalizeOrderHandler loads an order, checks that it is in the ready state, issues a certificate, then saves the order back, without holding any lock across those steps. Two finalize requests for the same order that arrive at the same time can both pass the ready check and each issue a separate certificate. The order only stores a single CertificateSerialNumber, so the last save wins and the other certificate is left orphaned from every order-keyed lookup, including /acme/order/<id>/cert and the account-based revocation path that walks orders. The orphaned certificate is fully valid for the named identifiers and cannot be found or revoked through the order.

RFC 8555 section 7.4 models finalize as a single transition (ready to processing to valid) that is meant to happen once per order.

Fix

Take a per-order lock from a striped locksutil pool around the finalize critical section, so finalize requests for the same order run one at a time. The lock pool lives on acmeState and is created in NewACMEState.

Finalize is forwarded to the active node (ForwardPerformanceStandby is set on the path), so all finalizes for a given order run on one node and an in-memory lock is enough to serialize them. The order keeps its existing pending to valid lifecycle, with ready and processing still computed on read by computeOrderStatus, so a failed issuance leaves the stored order untouched and the client can retry.

I looked at also persisting an intermediate processing status on disk, but Vault only ever persists orders as pending then valid (ready and processing are computed), and writing them back caused two problems: an order could get stuck in processing if the node failed over mid-issuance, and reverting to a stored ready would skip the authorization recheck that computeOrderStatus does for non-final orders. The lock on its own fixes the race without changing the persisted state machine.

Testing

Added tests in builtin/logical/pki/path_acme_order_test.go, all passing under -race:

  • TestACMEFinalizeOrder_ConcurrentRequestsIssueSingleCert runs several concurrent finalizes for one order and asserts exactly one succeeds, the rest are rejected with orderNotReady, and only one certificate is stored and referenced by the order.
  • TestACMEFinalizeOrder_FailedIssuanceLeavesOrderRetryable makes issuance fail once and confirms the order stays finalizable, then a retry succeeds.
  • TestACMEFinalizeOrder_OrderLockIsStablePerOrder checks the per-order lock is stable for a given order id.

Closes #31987

@Har1sh-k
Har1sh-k requested review from a team as code owners June 19, 2026 18:09
@Har1sh-k
Har1sh-k requested a review from sgmiller June 19, 2026 18:09
@vercel

vercel Bot commented Jun 19, 2026

Copy link
Copy Markdown

Deployment failed with the following error:

The `vercel.json` schema validation failed with the following message: should NOT have additional property `public`

Learn More: https://vercel.com/docs/concepts/projects/project-configuration

@dosubot dosubot Bot added the size:M This PR changes 30-99 lines, ignoring generated files. label Jun 19, 2026
@vercel

vercel Bot commented Jun 19, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
vault-ui Error Error Jun 19, 2026 6:09pm

Request Review

@dosubot dosubot Bot added the secret/pki label Jun 19, 2026
@hashicorp-cla-app

hashicorp-cla-app Bot commented Jun 19, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

…uance

acmeFinalizeOrderHandler loaded an order, checked it was in the ready state,
issued a certificate, and saved the order back, with no lock held across those
steps. Two concurrent finalize requests for the same order could both pass the
ready check and each issue a certificate. The order only records one serial
number, so the last save wins and the other certificate is left orphaned from
every order-keyed lookup, including /acme/order/<id>/cert and the account based
revocation path that walks it.

Take a per-order lock from a striped pool (locksutil) around the finalize
critical section so requests for the same order run one at a time. Finalize is
forwarded to the active node (ForwardPerformanceStandby), so all finalizes for
an order run on a single node and an in-memory lock is sufficient. The order
keeps its existing pending to valid lifecycle, with ready and processing still
computed on read, so a failed issuance leaves the stored order untouched and the
client can retry.

Add regression tests that run concurrent finalizes for one order and assert a
single certificate is issued and the rest are rejected, that a failed issuance
leaves the order finalizable so a retry succeeds, and that the per-order lock is
stable for a given order id.

Fixes hashicorp#31987
@hc-github-team-secure-vault-core

Copy link
Copy Markdown
Collaborator

Copy workflow completed!

From To Skipped Merge Commits Error
hashicorp/vault#32004 hashicorp/vault-enterprise#16533 0

@hc-github-team-secure-vault-core

Copy link
Copy Markdown
Collaborator

Copy pull request failed!

Origin Pull Request Copied Pull Request Commit SHA Error
#32004 https://github.com/hashicorp/vault-enterprise/pull/16533 https://github.com/hashicorp/vault-enterprise/commit/0b360d5b4986c231746ec6dbc62ad174d52cfcf7 PATCH https://api.github.com/repos/hashicorp/vault/issues/31987: 403 Resource not accessible by personal access token []
Closed Issues #31987
Error: PATCH https://api.github.com/repos/hashicorp/vault/issues/31987: 403 Resource not accessible by personal access token []

@ncabatoff

Copy link
Copy Markdown
Collaborator

Hi @Har1sh-k, thanks for the contribution! I really appreciate all the details you provided and the nice tests, they made it easy to review. I've merged the changes via a PR in our enterprise repo, as per our usual process. They won't be included in the next release because we missed the window on that, but they'll go out in the following one.

eualin pushed a commit to eualin/vault that referenced this pull request Jul 15, 2026
…ent double-issuance into main into ce/main (hashicorp#16550)

* Copy hashicorp#32004 into main



* secret/pki: lock ACME order finalize to prevent concurrent double-issuance

acmeFinalizeOrderHandler loaded an order, checked it was in the ready state,
issued a certificate, and saved the order back, with no lock held across those
steps. Two concurrent finalize requests for the same order could both pass the
ready check and each issue a certificate. The order only records one serial
number, so the last save wins and the other certificate is left orphaned from
every order-keyed lookup, including /acme/order/<id>/cert and the account based
revocation path that walks it.

Take a per-order lock from a striped pool (locksutil) around the finalize
critical section so requests for the same order run one at a time. Finalize is
forwarded to the active node (ForwardPerformanceStandby), so all finalizes for
an order run on a single node and an in-memory lock is sufficient. The order
keeps its existing pending to valid lifecycle, with ready and processing still
computed on read, so a failed issuance leaves the stored order untouched and the
client can retry.

Add regression tests that run concurrent finalizes for one order and assert a
single certificate is issued and the rest are rejected, that a failed issuance
leaves the order finalizable so a retry succeeds, and that the per-order lock is
stable for a given order id.

Fixes hashicorp#31987

* Rename CL

---------

Co-authored-by: Harish Kolla <hkolla03@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

secret/pki size:M This PR changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PKI ACME: acmeFinalizeOrderHandler lacks per-order locking — concurrent finalize double-issues and orphans a cert from order-keyed tracking

3 participants