Guide · Security
Sign your check-ins with HMAC
A plain ping URL is a bearer secret: anyone who sees it, in a log, a leaked crontab, or on the wire, can send a fake "up". Signed check-ins bind each ping to a shared secret and a timestamp, so a replayed or forged ping is rejected, even over plain HTTP.
Why sign your check-ins
A plain ping URL is a password anyone can use. If it leaks, through a log line, an exposed crontab, or someone watching the network, an attacker can send a fake all good and hide a real outage from you. The check stays green while your job is down.
Take a nightly job that rotates your TLS keys, and it starts failing. If someone can forge its check-in, they hold the check on a false up while your keys quietly expire, leaving vulnerable access open, and the alert you were counting on never fires. Or take a backup that stops running: a single leaked log line is enough to fake the done ping, so the outage stays hidden and you find the gap only when you go to restore.
Signed pings close that gap. Each check-in is tied to a secret and a timestamp, so a forged or replayed ping no longer matches and is refused. The only thing that can speak for your job is your job itself.
1. Turn on signed pings
Signed pings are available on every paid plan (Solo and up); they are not part of the Free plan. On the check's page, tick Require signed pings and save. Mortemain shows a per-check ping secret (a hex HMAC key) right below, copy it.
Turn this on after your job is signing, not before. Once Require signed pings is on, an unsigned ping to the URL is rejected and no longer counts as a check-in. Update the job to sign first, then flip the switch, otherwise you will trip your own alert.
Treat the ping secret like a password: it is per check, and anyone who holds it can sign for that check. Keep it out of Git, out of logs, and out of screenshots.
2. Sign a check-in (shell, openssl)
The secret is the HMAC key as raw bytes; openssl takes it as hexkey:. The signed message is three lines: the check UUID, a Unix timestamp, and the kind (success).
SECRET=your-hex-ping-secret URL="https://ping.mortemain.com/your-check-uuid" UUID=$(basename "$URL"); TS=$(date +%s) SIG=$(printf '%s\n%s\nsuccess' "$UUID" "$TS" | openssl dgst -sha256 -mac HMAC -macopt hexkey:$SECRET -r | cut -d' ' -f1) curl -H "X-Mortemain-Timestamp: $TS" -H "X-Mortemain-Signature: $SIG" "$URL"
3. Sign a check-in (PowerShell)
$secret = 'your-hex-ping-secret'; $url = 'https://ping.mortemain.com/your-check-uuid' $key = [byte[]]::new($secret.Length/2); for ($i=0; $i -lt $key.Length; $i++) { $key[$i] = [Convert]::ToByte($secret.Substring($i*2,2),16) } $uuid = $url.Split('/')[-1]; $ts = [int][double]::Parse((Get-Date -UFormat %s)) $sig = [BitConverter]::ToString(([Security.Cryptography.HMACSHA256]::new($key)).ComputeHash([Text.Encoding]::UTF8.GetBytes("$uuid`n$ts`nsuccess"))).Replace('-','').ToLower() Invoke-RestMethod $url -Headers @{ 'X-Mortemain-Timestamp' = $ts; 'X-Mortemain-Signature' = $sig }
Your dashboard generates both of these for you, filled in with the real secret and URL, on the check's page once signing is on.
4. Sign a check-in (Python)
Standard library only, no dependencies. Same three-line message, same two headers.
import hmac, hashlib, time, urllib.request secret = bytes.fromhex("your-hex-ping-secret") url = "https://ping.mortemain.com/your-check-uuid" uuid = url.rsplit("/", 1)[-1] ts = str(int(time.time())) msg = f"{uuid}\n{ts}\nsuccess".encode() sig = hmac.new(secret, msg, hashlib.sha256).hexdigest() req = urllib.request.Request(url, headers={ "X-Mortemain-Timestamp": ts, "X-Mortemain-Signature": sig, }) urllib.request.urlopen(req)
5. The rule (any language)
To sign from any language, reproduce this exactly:
signature = hex( HMAC-SHA256( secret, uuid + "\n" + timestamp + "\n" + kind ) )
secret— the check's ping secret, decoded from hex to raw bytes as the HMAC key.uuid— the last path segment of the ping URL.timestamp— current Unix time in seconds, sent verbatim in theX-Mortemain-Timestampheader. It must be within about five minutes of the server clock, so keep the client roughly in sync (NTP). This is what stops a captured signature being replayed later.kind—success,start,fail, orlog. Sign the kind you are sending.- Send the hex signature in the
X-Mortemain-Signatureheader.
For /start or /fail, append it to the URL and sign the matching kind, e.g. a signed failure:
SIG=$(printf '%s\n%s\nfail' "$UUID" "$TS" | openssl dgst -sha256 -mac HMAC -macopt hexkey:$SECRET -r | cut -d' ' -f1) curl -H "X-Mortemain-Timestamp: $TS" -H "X-Mortemain-Signature: $SIG" "$URL/fail"
Test it
Run the signed curl once by hand. The check should flip to up within a second or two. Now change one character of the signature and try again, it should be rejected and the check stays down. That rejection is the whole point: a stolen or replayed ping URL can no longer lie about your job being alive.
FAQ
Can I use the same secret for several checks?
No. Each check has its own ping secret, shown on its page once signing is on. A secret only ever signs for its own check, so a leak is contained to that one.
What if my timestamp is too old?
Mortemain refuses the ping and treats it as a possible replay. Signatures are accepted only within about five minutes of the server clock, so keep the sending machine synced with NTP.
Can I sign /start and /fail too?
Yes. Append /start or /fail to the URL and sign that same kind (start or fail) in the message. The kind you sign has to match the one you send, or the ping is rejected.
Going deeper
- RFC 2104 — HMAC: Keyed-Hashing for Message Authentication, the specification this scheme follows.
- HMAC (Wikipedia), a plainer-language overview if the RFC is heavy going.