Skip to content

Commit 20ad838

Browse files
authored
Force rotation: adds an integration test to verify that the JWT autho… (#5583)
* Force rotation: adds an integration test to verify that the JWT authority correctly handles forced rotation. Ensures that JWT tokens are invalidated and reissued as expected. Signed-off-by: Marcos Yacob <marcosyacob@gmail.com>
1 parent b80bf4e commit 20ad838

17 files changed

+331
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#!/bin/bash
2+
3+
set -e
4+
5+
"${ROOTDIR}/setup/x509pop/setup.sh" conf/server conf/agent
6+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/bin/bash
2+
3+
docker-up spire-server
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#!/bin/bash
2+
3+
log-debug "bootstrapping agent..."
4+
docker compose exec -T spire-server \
5+
/opt/spire/bin/spire-server bundle show > conf/agent/bootstrap.crt
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/bin/bash
2+
3+
docker-up spire-agent
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#!/bin/bash
2+
3+
log-debug "creating registration entry..."
4+
docker compose exec -T spire-server \
5+
/opt/spire/bin/spire-server entry create \
6+
-parentID "spiffe://domain.test/spire/agent/x509pop/$(fingerprint conf/agent/agent.crt.pem)" \
7+
-spiffeID "spiffe://domain.test/workload" \
8+
-selector "unix:uid:0" \
9+
-x509SVIDTTL 0
10+
check-synced-entry "spire-agent" "spiffe://domain.test/workload"
11+
12+
log-info "checking X509-SVID"
13+
docker compose exec -T spire-agent \
14+
/opt/spire/bin/spire-agent api fetch x509 || fail-now "SVID check failed"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#!/bin/bash
2+
3+
# Initial check for x509 authorities in spire-server
4+
jwt_authorities=$(docker compose exec -T spire-server \
5+
/opt/spire/bin/spire-server bundle show -output json | jq '.jwt_authorities' -c)
6+
7+
amount_authorities=$(echo "$jwt_authorities" | jq length)
8+
9+
# Ensure only one JWT authority is present at the start
10+
if [[ $amount_authorities -ne 1 ]]; then
11+
fail-now "Only one JWT authority expected at start"
12+
fi
13+
14+
# Prepare authority
15+
prepared_authority_id=$(docker compose exec -T -e SPIRE_SERVER_FFLAGS=forced_rotation spire-server \
16+
/opt/spire/bin/spire-server localauthority jwt prepare -output json | jq -r .prepared_authority.authority_id)
17+
18+
# Verify that the prepared authority is logged
19+
searching="JWT key prepared|local_authority_id=${prepared_authority_id}"
20+
check-log-line spire-server "$searching"
21+
22+
# Check for updated x509 authorities in spire-server
23+
# Check for updated JWT authorities in spire-server
24+
jwt_authorities=$(docker compose exec -T spire-server \
25+
/opt/spire/bin/spire-server bundle show -output json | jq '.jwt_authorities' -c)
26+
amount_authorities=$(echo "$jwt_authorities" | jq length)
27+
28+
# Ensure two JWT authorities are present after preparation
29+
if [[ $amount_authorities -ne 2 ]]; then
30+
fail-now "Two JWT authorities expected after prepare"
31+
fi
32+
33+
# Ensure the prepared authority is present
34+
if ! echo "$jwt_authorities" | jq -e ".[] | select(.key_id == \"$prepared_authority_id\")" > /dev/null; then
35+
fail-now "Prepared authority not found"
36+
fi
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
#!/bin/bash
2+
3+
prepared_authority=$(docker compose exec -t -e SPIRE_SERVER_FFLAGS=forced_rotation spire-server \
4+
/opt/spire/bin/spire-server \
5+
localauthority jwt show -output json | jq -r .active.authority_id) || fail-now "Failed to fetch prepared JWT authority ID"
6+
7+
svid_json=$(docker compose exec spire-agent ./bin/spire-agent \
8+
api fetch jwt -audience aud -output json) || fail-now "Failed to fetch JWT SVID"
9+
10+
jwt_svid=$(echo $svid_json | jq -c '.[0].svids[0].svid') || fail-now "Failed to parse JWT SVID"
11+
12+
# Store JWT SVID for the next steps
13+
echo $jwt_svid > conf/agent/jwt_svid
14+
15+
# Extract key ID from JWT SVID
16+
skid=$(echo "$jwt_svid" | jq -r 'split(".") | .[0] | @base64d | fromjson | .kid')
17+
18+
# Check if the key ID matches the prepared authority ID
19+
if [[ $skid != $prepared_authority ]]; then
20+
fail-now "JWT SVID key ID does not match the prepared authority ID, got $skid, expected $prepared_authority"
21+
fi
22+
23+
keys=$(echo $svid_json | jq -c '.[1].bundles["spiffe://domain.test"] | @base64d | fromjson')
24+
25+
retry_count=0
26+
max_retries=20
27+
success=false
28+
29+
while [[ $retry_count -lt $max_retries ]]; do
30+
keysLen=$(echo $keys | jq -c '.keys | length')
31+
if [[ $keysLen -eq 2 ]]; then
32+
success=true
33+
break
34+
else
35+
echo "Retrying... ($((retry_count+1))/$max_retries)"
36+
retry_count=$((retry_count+1))
37+
sleep 2
38+
# Re-fetch the JWT SVID and keys
39+
svid_json=$(docker compose exec spire-agent ./bin/spire-agent \
40+
api fetch jwt -audience aud -output json) || fail-now "Failed to re-fetch JWT SVID"
41+
jwt_svid=$(echo $svid_json | jq -c '.[0].svids[0].svid') || fail-now "Failed to parse re-fetched JWT SVID"
42+
keys=$(echo $svid_json | jq -c '.[1].bundles["spiffe://domain.test"] | @base64d | fromjson')
43+
fi
44+
done
45+
46+
if [[ $success == false ]]; then
47+
fail-now "Expected one key in JWT SVID bundle, got $keysLen after $max_retries retries"
48+
fi
49+
50+
echo $keys | jq --arg kid $prepared_authority -e '.keys[] | select(.kid == $kid)' > /dev/null || fail-now "Prepared authority not found in JWT SVID bundle"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#!/bin/bash
2+
3+
# Fetch the prepared authority ID
4+
prepared_authority=$(docker compose exec -t -e SPIRE_SERVER_FFLAGS=forced_rotation spire-server \
5+
/opt/spire/bin/spire-server \
6+
localauthority jwt show -output json | jq -r .prepared.authority_id) || fail-now "Failed to fetch prepared JWT authority ID"
7+
8+
# Activate the authority
9+
activated_authority=$(docker compose exec -t -e SPIRE_SERVER_FFLAGS=forced_rotation spire-server \
10+
/opt/spire/bin/spire-server \
11+
localauthority jwt activate -authorityID "${prepared_authority}" \
12+
-output json | jq -r .activated_authority.authority_id) || fail-now "Failed to activate JWT authority"
13+
14+
log-info "Activated authority: ${activated_authority}"
15+
16+
# Check logs for specific lines
17+
check-log-line spire-server "JWT key activated|local_authority_id=${prepared_authority}"
18+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#!/bin/bash
2+
3+
check-logs() {
4+
local component=$1
5+
shift
6+
for log in "$@"; do
7+
check-log-line "$component" "$log"
8+
done
9+
}
10+
11+
# Fetch old authority ID
12+
old_jwt_authority=$(docker compose exec -T -e SPIRE_SERVER_FFLAGS=forced_rotation spire-server \
13+
/opt/spire/bin/spire-server \
14+
localauthority jwt show -output json | jq -r .old.authority_id) || fail-now "Failed to fetch old authority ID"
15+
16+
log-debug "Old authority: $old_jwt_authority"
17+
18+
# Taint the old authority
19+
docker compose exec -T -e SPIRE_SERVER_FFLAGS=forced_rotation spire-server \
20+
/opt/spire/bin/spire-server \
21+
localauthority jwt taint -authorityID "${old_jwt_authority}" || fail-now "Failed to taint old authority"
22+
23+
# check Server logs
24+
check-logs spire-server \
25+
"JWT authority tainted successfully|local_authority_id=${old_jwt_authority}"
26+
27+
# Check Agent logs
28+
check-logs spire-agent \
29+
"JWT-SVIDs were removed from the JWT cache because they were issued by a tainted authority|count_jwt_svids=1|jwt_authority_key_ids=${old_jwt_authority}"
30+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#!/bin/bash
2+
3+
active_authority=$(docker compose exec -t -e SPIRE_SERVER_FFLAGS=forced_rotation spire-server \
4+
/opt/spire/bin/spire-server \
5+
localauthority jwt show -output json | jq -r .active.authority_id) || fail-now "Failed to fetch active JWT authority ID"
6+
7+
jwt_svid=$(docker compose exec spire-agent ./bin/spire-agent \
8+
api fetch jwt -audience aud -output json | jq -c '.[0].svids[0].svid') || fail-now "Failed to fetch JWT SVID"
9+
10+
oldJWT=$(cat conf/agent/jwt_svid)
11+
if [[ $oldJWT == $jwt_svid ]]; then
12+
fail-now "JWT SVID did not rotate"
13+
fi
14+
15+
# Extract key ID from JWT SVID
16+
skid=$(echo "$jwt_svid" | jq -r 'split(".") | .[0] | @base64d | fromjson | .kid')
17+
18+
# Check if the key ID matches the active authority ID
19+
if [[ $skid != $active_authority ]]; then
20+
fail-now "JWT SVID key ID does not match the active authority ID, got $skid, expected $active_authority"
21+
fi
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#!/bin/bash
2+
3+
old_jwt_authority=$(docker compose exec -T -e SPIRE_SERVER_FFLAGS=forced_rotation spire-server \
4+
/opt/spire/bin/spire-server \
5+
localauthority jwt show -output json | jq -r .old.authority_id) || fail-now "Failed to fetch old authority ID"
6+
7+
log-debug "Old authority: $old_jwt_authority"
8+
9+
jwt_authorities_count=$(docker compose exec -T spire-server \
10+
/opt/spire/bin/spire-server bundle \
11+
show -output json | jq '.jwt_authorities | length')
12+
13+
if [ $jwt_authorities_count -eq 2 ]; then
14+
log-debug "Two JWT Authorities found"
15+
else
16+
fail-now "Expected to be two JWT Authorities. Found $jwt_authorities_count."
17+
fi
18+
19+
tainted_found=$(docker compose exec -T spire-server /opt/spire/bin/spire-server bundle show -output json | jq '.jwt_authorities[] | select(.tainted == true)')
20+
21+
if [[ -z "$tainted_found" ]]; then
22+
fail-now "Tainted JWT authority expected"
23+
fi
24+
25+
docker compose exec -T -e SPIRE_SERVER_FFLAGS=forced_rotation spire-server \
26+
/opt/spire/bin/spire-server localauthority jwt \
27+
revoke -authorityID $old_jwt_authority -output json || fail-now "Failed to revoke JWT authority"
28+
29+
check-log-line spire-server "JWT authority revoked successfully|local_authority_id=$old_jwt_authority"
30+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
#!/bin/bash
2+
3+
for i in {1..20}; do
4+
active_jwt_authority=$(docker compose exec -T -e SPIRE_SERVER_FFLAGS=forced_rotation spire-server \
5+
/opt/spire/bin/spire-server \
6+
localauthority jwt show -output json | jq -r .active.authority_id) || fail-now "Failed to fetch old jwt authority ID"
7+
8+
log-debug "Active old authority: $active_jwt_authority"
9+
10+
svid_json=$(docker compose exec spire-agent ./bin/spire-agent \
11+
api fetch jwt -audience aud -output json)
12+
13+
keys=$(echo $svid_json | jq -c '.[1].bundles["spiffe://domain.test"] | @base64d | fromjson')
14+
15+
keysLen=$(echo $keys | jq -c '.keys | length')
16+
if [[ $keysLen -eq 1 ]]; then
17+
break
18+
fi
19+
20+
if [[ $i -eq 20 ]]; then
21+
fail-now "Expected one key in JWT SVID bundle, got $keysLen after 20 attempts"
22+
fi
23+
24+
sleep 2s
25+
done
26+
27+
echo $keys | jq --arg kid $active_jwt_authority -e '.keys[] | select(.kid == $kid)' > /dev/null || fail-now "Active authority not found in JWT SVID bundle"
28+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Force rotation with JWT Authority Test Suite
2+
3+
## Description
4+
5+
This test suite configures a single SPIRE Server and Agent to validate the forced rotation and revocation of JWT authorities.
6+
7+
## Test steps
8+
9+
1. **Prepare a new JWT authority**: Verify that a new JWT authority is successfully created.
10+
2. **Activate the new JWT authority**: Ensure that the new JWT authority becomes the active authority.
11+
3. **Taint the old JWT authority**: Confirm that the old JWT authority is marked as tainted, and verify that the taint instruction is propagated to the agent, triggering the deletion of any JWT-SVID signed by tainted authority.
12+
4. **Revoke the tainted JWT authority**: Validate that the revocation instruction is propagated to the agent and that all the JWT-SVIDs have the revoked authority removed.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
agent {
2+
data_dir = "/opt/spire/data/agent"
3+
log_level = "DEBUG"
4+
server_address = "spire-server"
5+
server_port = "8081"
6+
trust_bundle_path = "/opt/spire/conf/agent/bootstrap.crt"
7+
trust_domain = "domain.test"
8+
}
9+
10+
plugins {
11+
NodeAttestor "x509pop" {
12+
plugin_data {
13+
private_key_path = "/opt/spire/conf/agent/agent.key.pem"
14+
certificate_path = "/opt/spire/conf/agent/agent.crt.pem"
15+
}
16+
}
17+
KeyManager "disk" {
18+
plugin_data {
19+
directory = "/opt/spire/data/agent"
20+
}
21+
}
22+
WorkloadAttestor "unix" {
23+
plugin_data {
24+
}
25+
}
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
server {
2+
bind_address = "0.0.0.0"
3+
bind_port = "8081"
4+
trust_domain = "domain.test"
5+
data_dir = "/opt/spire/data/server"
6+
log_level = "DEBUG"
7+
ca_ttl = "24h"
8+
default_jwt_svid_ttl = "8h"
9+
experimental {
10+
feature_flags = ["forced_rotation"]
11+
}
12+
}
13+
14+
plugins {
15+
DataStore "sql" {
16+
plugin_data {
17+
database_type = "sqlite3"
18+
connection_string = "/opt/spire/data/server/datastore.sqlite3"
19+
}
20+
}
21+
NodeAttestor "x509pop" {
22+
plugin_data {
23+
ca_bundle_path = "/opt/spire/conf/server/agent-cacert.pem"
24+
}
25+
}
26+
KeyManager "memory" {
27+
plugin_data = {}
28+
}
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
services:
2+
spire-server:
3+
image: spire-server:latest-local
4+
hostname: spire-server
5+
volumes:
6+
- ./conf/server:/opt/spire/conf/server
7+
command: ["-config", "/opt/spire/conf/server/server.conf"]
8+
spire-agent:
9+
image: spire-agent:latest-local
10+
hostname: spire-agent
11+
depends_on: ["spire-server"]
12+
volumes:
13+
- ./conf/agent:/opt/spire/conf/agent
14+
command: ["-config", "/opt/spire/conf/agent/agent.conf"]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#!/bin/bash
2+
3+
if [ -z "$SUCCESS" ]; then
4+
docker compose logs
5+
fi
6+
docker-down

0 commit comments

Comments
 (0)