Add optional mTLS support to the tang pin#566
Conversation
Allow the tang pin to authenticate to the Tang server with mutual TLS. Three new optional config properties are accepted by clevis-encrypt-tang: cacert - CA bundle to verify the server certificate (curl --cacert) cert - client certificate for mTLS (curl --cert) key - private key for the client certificate (curl --key) When none are set, behaviour is unchanged. The paths are persisted into the JWE protected header so clevis-decrypt-tang reuses them for the recovery (POST /rec) request without extra configuration. Add a pin-tang-mtls integration test (TLS-fronted tangd via socat with mandatory client-cert verification) plus tang_generate_certs and tang_run_mtls helpers. The test skips cleanly when openssl or a socat built with OpenSSL support is unavailable.
|
/packit test |
|
@sarroutbi I could not see the detailed error message of centos-stream-10-x86_64:pkcs11 failure, but seems like it is timeout issue, could you help to take a look if this is related with my PR changes? |
|
/packit retest-failed |
| on_exit() { | ||
| exit_status=$? | ||
| tang_stop "${TMP}" | ||
| [ -d "$TMP" ] && rm -rf "$TMP" | ||
| exit "${exit_status}" | ||
| } |
There was a problem hiding this comment.
The on_exit trap is registered (line 29) before tang_mtls_sanity_check (line 32), which runs before TMP is assigned (line 34). If the sanity check calls skip_test, the trap fires with TMP unset, and tang_stop "" ends up calling error "tang_stop: please specify 'basedir'" which exits 1. That converts a SKIP (exit 77) into a FAIL (exit 1).
This will break CI on systems that have socat+tangd but socat was built without OpenSSL support.
Compare with pin-tang, where TMP="$(mktemp -d)" is set right after the trap with no skip-able call in between.
Please guard the cleanup so it tolerates an unset TMP:
| on_exit() { | |
| exit_status=$? | |
| tang_stop "${TMP}" | |
| [ -d "$TMP" ] && rm -rf "$TMP" | |
| exit "${exit_status}" | |
| } | |
| on_exit() { | |
| exit_status=$? | |
| [ -n "${TMP}" ] && tang_stop "${TMP}" | |
| [ -d "${TMP}" ] && rm -rf "$TMP" | |
| exit "${exit_status}" | |
| } |
| # (plain HTTP/TLS, no client certificate and default CA verification). | ||
| cacert="$(jose fmt -j- -Og cacert -Su- <<< "$cfg")" || true | ||
| cert="$(jose fmt -j- -Og cert -Su- <<< "$cfg")" || true | ||
| key="$(jose fmt -j- -Og key -Su- <<< "$cfg")" || true |
There was a problem hiding this comment.
These paths are never validated for existence before use. The existing adv file path is checked (line 105: if ! [ -f "$jws" ]), so there is a consistency gap.
If a user typos a path (e.g., clietn.crt), two things can happen:
-
When the advertisement is fetched via curl,
curl -sfgfails silently and the user sees"Unable to fetch advertisement", which points them at network/DNS issues instead of the real problem. -
When
advis provided inline, curl is never called, encryption succeeds, and the bad path gets permanently baked into the JWE header. Every decrypt attempt then fails at boot with"Error communicating with server".
Please add file-existence checks after extracting the paths, matching the adv validation pattern:
[ -n "$cacert" ] && [ ! -f "$cacert" ] && { echo "CA cert file '$cacert' not found!" >&2; exit 1; }
[ -n "$cert" ] && [ ! -f "$cert" ] && { echo "Client cert file '$cert' not found!" >&2; exit 1; }
[ -n "$key" ] && [ ! -f "$key" ] && { echo "Client key file '$key' not found!" >&2; exit 1; }|
|
||
| # Optional (m)TLS parameters. Absent for JWEs created without mTLS, in which | ||
| # case curl behaves exactly as before. | ||
| cacert="$(jose fmt -j- -Og clevis -g tang -g cacert -Su- <<< "$jhd")" || true |
There was a problem hiding this comment.
During decryption, cacert, cert, key (and url) are all extracted from the JWE protected header and fed to curl (line 115) before the AEAD integrity check at line 128 (jose jwe dec). The url field was already consumed unauthenticated before this patch, so this is a pre-existing design property of clevis-tang. However, cacert qualitatively extends the attack surface: an attacker who can modify the stored JWE can now also override which CA curl trusts, enabling MITM against the legitimate tang server with a self-signed cert planted on disk. Previously, modifying url alone still required a certificate trusted by the system CA store.
The practical exploitability is limited (requires disk write access + network position, and the ECMR exchange value alone does not reveal the decryption key), so this may be acceptable as a known trade-off.
Worth considering: validate that cert/key/cacert paths are under an expected prefix (e.g., /etc/clevis/), or use indirection (store a profile name, resolve to paths at runtime from a fixed directory). At minimum, document the trust model explicitly so users understand the implication.
| ct="Content-Type: application/jwk+json" | ||
| if ! rep="$(curl -sfg -X POST -H "$ct" --data-binary @- "$rec_url" <<< "$xfr")"; then | ||
| if ! rep="$(curl -sfg "${curl_opts[@]}" -X POST -H "$ct" --data-binary @- "$rec_url" <<< "$xfr")"; then | ||
| echo "Error communicating with server $url" >&2 |
There was a problem hiding this comment.
When mTLS parameters are present in the JWE header but the referenced files no longer exist (cert rotation, initramfs rebuilt without them), curl -sfg fails silently and all the user sees is this generic message. In the initramfs at boot, this sends the operator chasing network/firewall issues when the real problem is a missing certificate file.
The man page already warns about ensuring files are in the initramfs, which tells us this is a known footgun. Would it make sense to check the files exist before the curl call and produce a targeted diagnostic?
[ -n "$cacert" ] && [ ! -f "$cacert" ] && { echo "mTLS CA cert '$cacert' not found (is it in the initramfs?)" >&2; exit 1; }
[ -n "$cert" ] && [ ! -f "$cert" ] && { echo "mTLS client cert '$cert' not found (is it in the initramfs?)" >&2; exit 1; }
[ -n "$key" ] && [ ! -f "$key" ] && { echo "mTLS client key '$key' not found (is it in the initramfs?)" >&2; exit 1; }| curl_opts=() | ||
| [ -n "$cacert" ] && curl_opts+=(--cacert "$cacert") | ||
| [ -n "$cert" ] && curl_opts+=(--cert "$cert") | ||
| [ -n "$key" ] && curl_opts+=(--key "$key") |
There was a problem hiding this comment.
Specifying key without cert is a no-op: curl ignores --key when --cert is not also provided. If the server requires mTLS, the handshake fails with a generic error and there is no indication that cert was missing. Please add a consistency check:
[ -n "$key" ] && [ -z "$cert" ] && { echo "'key' requires 'cert' to also be set!" >&2; exit 1; }
Summary
Adds optional mutual-TLS (mTLS) support to the Tang pin, allowing Clevis to authenticate to a Tang server (typically fronted by a TLS-terminating reverse proxy) with a client certificate, and to verify the server certificate against a custom CA.
The feature is fully opt-in: when none of the new properties are set, the pin behaves exactly as before (plain HTTP/HTTPS with the system's default CA trust). Existing JWEs and workflows are unaffected.
Motivation
Adding mTLS lets the Tang server (typically via a TLS-terminating reverse proxy) authenticate the client's identity through its client certificate. This unlocks per-machine access control on top of the existing network-binding model. In particular, single-machine revocation: revoking or refusing a specific client certificate immediately prevents that one machine from recovering its keys, without affecting any other machine or requiring key rotation on the server. This matches how we want to adopt Clevis out of the box at Meta: bind machines to Tang for automated unlock, and retain the ability to revoke an individual host's recovery access by its identity. It's also useful more generally for any deployment where the Tang server is fronted by HTTPS with client-certificate authentication, or uses a server certificate signed by a private/internal CA.
What changed
Three new optional config properties are accepted by clevis encrypt tang:
When any of above are set:
Example:
Backward compatibility
Tests