TLDR: Signing API requests from Bash is tricky, but doable with a temporary file.
Every couple of months I rotate the DKIM keys of my email server, after which I publish them on my website. This article on publishing dkim keys gives a good overview of why this is a good idea.
Initially this was all done manually, but over time I automated more and more. The toughest part was finding a good DNS registrar (DKIM keys are published in DNS), that has a proper API, and then using that API from Bash. The DNS registrar I am using is TransIP.
Here is how I did it.
Before we can use any other endpoint of the API, we need to get a token. We get the token by sending an API request with your username. The request must be signed with the private key that you uploaded/obtained from the API part of the TransIP console.
Using Bash variables to hold the request makes signing very tricky, before you know it a newline is added or removed, invalidating the signature; the transmitted request must be byte-for-byte the same as what was signed. Instead, we side step all Bash idiosycrasies by storing the request in a temporary file. Here we go:
# Configure your TransIP username and location of the private key.
TRANSIP_USERNAME=your-username
TRANSIP_PRIVATE_KEY=/path/to/your-transip-private-key.pem
# The temporary file that holds the request.
TOKEN_REQUEST_BODY=$(mktemp)
# Create the request from your username.
# We're going to write DNS entries so 'read_only' must be 'false'.
# The request also needs a random nonce.
# The token is only needed for a short time, 30 seconds is enough
# in a Bash script.
# I vagely remember that the label must be unique, so some randomness
# is added there as well.
cat <<EOF > "$TOKEN_REQUEST_BODY"
{
"login": "$TRANSIP_USERNAME",
"nonce": "$(openssl rand -base64 15)",
"read_only": false,
"expiration_time": "30 seconds",
"label": "Add dkim dns entry $RANDOM",
"global_key": true
}
EOF
# Sign the request with openssl and encode the signature in base64.
SIGNATURE=$(
cat "$TOKEN_REQUEST_BODY" |
openssl dgst -sha512 -sign $TRANSIP_PRIVATE_KEY |
base64 --wrap=0
)
# Send the request with curl.
# Note how we use '--data-binary' option to make sure curl transmit
# the request byte-for-byte as it was generated.
TOKEN_JSON=$(
curl \
--silent \
--show-error \
-X POST \
-H "Content-Type: application/json" \
-H "SIGNATURE: $SIGNATURE" \
--data-binary "@$TOKEN_REQUEST_BODY" \
https://api.transip.nl/v6/auth
)
rm -rf $TOKEN_REQUEST_BODY
# Extract the TOKEN from the response using jq.
TOKEN=$(echo "$TOKEN_JSON" | jq --raw-output .token)
if [[ "$TOKEN" == "null" ]]; then
echo "Failed to get token"
echo "$TOKEN_JSON"
exit 1
fi
Now we can collect the data to write a DNS entry:
DNS_DOMAIN="your-domain.com"
DNS_NAME="unique-dkim-key-name._domainkey"
DNS_VALUE="v=DKIM1; h=sha256; t=s; p=MIIBIjANBgkqhkiG9....DAQAB"
I am using amavisd for DKIM so the values can be fetched with some grep/awk trickery:
DNS_DOMAIN="my-domain.com"
DNS_NAME=$(amavisd showkeys | grep -o '^[^.]*._domainkey')
DNS_VALUE=$(amavisd showkeys | awk -F'"' '$2 != "" {VALUE=VALUE $2}; END {print VALUE}')
Now we can create the DNS entry for the DKIM key:
# Create the request.
DNS_REQUEST_BODY=$(
cat <<EOF
{"dnsEntry":{"name":"${DNS_NAME}","expire":86400,"type":"TXT","content":"${DNS_VALUE}"}}
EOF
)
# Send the request with curl.
REGISTER_RESULT="$(
curl \
--silent \
--show-error \
-X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d "$DNS_REQUEST_BODY" \
"https://api.transip.nl/v6/domains/${DNS_DOMAIN}/dns"
)"
if [[ "$REGISTER_RESULT" != "[]" ]]; then
echo "Failed to register new DKIM DNS entry"
echo "$REGISTER_RESULT"
exit 1
fi
Note that this time this request is stored in a Bash variable.
Update 2024-01-50: Constructed variable DNS_REQUEST_BODY
with cat instead of read because the latter exists with a non-zero exit code causing the script to exit.