Signing applies to
http drains only. OTLP, Datadog, and Better Stack deliveries carry no X-Lua-Signature header — those destinations authenticate the sender with the API key or token you configured.The header
During a rotation window the header carries two
v1 values. The active secret’s signature is always first. Accept a request when any v1 matches any secret you hold.
What is signed
- The separator is a literal
.between the timestamp and the body. The timestamp is the decimal integer fromt, with no padding. - Reject a header that is not in canonical form, rather than coercing it.
tis0or a digit string with no leading zero, no sign, no fractional part and no0xprefix; eachv1is exactly 64 lowercase hex characters. Lua sends nothing else, and a lenient parser is one an attacker can steer. - Compare in constant time, and compare bytes. Use
timingSafeEqual,hmac.compare_digest, orhmac.Equal. A plain==on the hex string leaks how much of your secret an attacker has guessed. - Reject a timestamp more than 5 minutes from now, in either direction. The tolerance is 300 seconds and it is inclusive — exactly 300 seconds of skew still verifies. That is the replay window; the platform never signs a batch it will not send within it.
Test vector
Check your implementation against this before you wire it to a real drain. The body is a single line with no trailing newline.Verifiers
Each of these accepts a list of secrets — the active one and, during a rotation, the previous one — and each checks everyv1 against every secret without returning early, so its timing says nothing about which one matched: the request is accepted when any v1 matches any secret you hold. All three reject a non-canonical t and a v1 that is not 64 lowercase hex characters before doing any work, which is the behaviour the platform’s own reference verifiers implement.
Rotate a secret
Rotation overlaps two secrets so there is no window where deliveries fail. The platform signs with both and your receiver accepts either.1
Start the rotation
Output
v1= values: the new secret’s first, the previous secret’s second. The window is 24 hours.2
Roll the new secret out
Put the new value where your verifier reads the active secret, and move the old value to where it reads the previous one. The verifiers above take a list, so both are accepted throughout.Deploy, then confirm with a test delivery:
3
Close the window
v1 value, signed with the new secret. Remove the old value from your receiver. If you do nothing, the window closes on its own after 24 hours and has the same effect.DRAIN_ROTATION_IN_PROGRESS. Finalize the first one, or pass --finalize to close it and open a new one in the same step.
If it isn’t working
Every signature mismatches
Every signature mismatches
Almost always the compressed body. Compute the HMAC over the bytes after gunzip, not over what arrived. Check by logging the byte length you are signing: it should match the batch’s JSON length, not the smaller compressed length.
It matched yesterday and fails today
It matched yesterday and fails today
A rotation is open and your receiver takes only one secret, matching the first
v1 it finds. The active secret’s signature is first, so a receiver still holding only the previous secret fails every request. Accept a list of secrets, and check every v1 against every one of them.Intermittent skew failures
Intermittent skew failures
Your host’s clock has drifted. The tolerance is 5 minutes in either direction, which is generous; if you are failing inside it, run NTP. Do not widen the window past 5 minutes — that is the replay protection.
I lost the secret
I lost the secret
It cannot be read back; it is shown once at creation and once per rotation. Run
lua drains rotate-secret <id> to mint a new one and deploy it. Because rotation overlaps, nothing is lost while you do it — but you will not be able to verify with the old value, so finalize as soon as the new one is live.Next steps
Generic HTTPS
The full request contract and ownership verification.
Event schema
What is inside the body you just verified.
Delivery guarantees
At-least-once, retry, and drop semantics.
Security and data
What the platform scrubs and keeps.

