Skip to content

Commit c04635c

Browse files
committed
Add E2E tests for OIDC token validation
This adds end-to-end tests and supporting material to execute tests that validate parts of the OIDC functionality. Signed-off-by: Allain Legacy <[email protected]>
1 parent fdc1002 commit c04635c

33 files changed

+1196
-6
lines changed

Makefile

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ PKGS=$(shell go list ./... | grep -v /test/e2e)
1515
DOCKER_REPO?=quay.io/brancz/kube-rbac-proxy
1616
KUBECONFIG?=$(HOME)/.kube/config
1717
CONTAINER_NAME?=$(DOCKER_REPO):$(VERSION)
18+
KRP_CURL_OIDC_OVERRIDES=$(shell cat ./test/data/krp-curl-oidc-overrides.json)
1819

1920
ALL_ARCH=amd64 arm arm64 ppc64le s390x
2021
ALL_PLATFORMS=$(addprefix linux/,$(ALL_ARCH))
@@ -83,7 +84,11 @@ curl-container:
8384

8485
run-curl-container:
8586
@echo 'Example: curl -v -s -k -H "Authorization: Bearer `cat /var/run/secrets/kubernetes.io/serviceaccount/token`" https://kube-rbac-proxy.default.svc:8443/metrics'
86-
kubectl run -i -t krp-curl --image=quay.io/brancz/krp-curl:v0.0.2 --restart=Never --command -- /bin/sh
87+
kubectl run -i -t --rm krp-curl --image=quay.io/brancz/krp-curl:v0.0.2 --restart=Never --command -- /bin/sh
88+
89+
run-oidc-curl-container:
90+
@echo 'Example: curl -v -s --cert /certs/tls.crt --key /certs/tls.key --cacert /certs/ca.crt -H "Authorization: Bearer `cat /tokens/token.jwt`" https://kube-rbac-proxy.default.svc.cluster.local:8443/metrics'
91+
kubectl run -i -t --rm krp-curl --image=quay.io/brancz/krp-curl:v0.0.2 --overrides='$(KRP_CURL_OIDC_OVERRIDES)' --restart=Never --command -- /bin/sh
8792

8893
grpcc-container:
8994
docker build -f ./examples/grpcc/Dockerfile -t mumoshu/grpcc:v0.0.1 .

scripts/generate-certificates.sh

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
#!/bin/bash
2+
3+
set -e
4+
5+
E2E_DATA=./test/e2e
6+
TEST_DATA=./test/data
7+
WORKDIR=$(mktemp -d)
8+
9+
trap 'rm -rf -- "${WORKDIR}"' EXIT
10+
11+
if [ ! -d .git ]; then
12+
echo "This script must be run from the top-level directory of the repository."
13+
exit 1
14+
fi
15+
16+
pushd "${WORKDIR}" > /dev/null
17+
18+
# Setup a certificate for our test CA
19+
openssl req -x509 -sha256 -days 3650 -newkey rsa:2048 -keyout ca.key -out ca.crt -noenc \
20+
-subj "/CN=kube-rbac-proxy-signer" \
21+
-addext "keyUsage=digitalSignature,keyEncipherment,cRLSign,keyCertSign" > /dev/null 2>&1
22+
CA_CERT=ca.crt
23+
CA_KEY=ca.key
24+
25+
# Setup a certificate for the test client
26+
openssl genrsa -out client.key 2048
27+
openssl req -key client.key -new -out client.csr -subj '/CN=kube-rbac-proxy-certificates-test'
28+
openssl x509 -req -CA "${CA_CERT}" -CAkey "${CA_KEY}" -in client.csr -out client.crt -days 3650 -CAcreateserial \
29+
-extensions client \
30+
-extfile <(
31+
cat <<EOF
32+
[client]
33+
basicConstraints = CA:FALSE
34+
extendedKeyUsage = clientAuth
35+
EOF
36+
) 2> /dev/null
37+
38+
# Setup a certificate for the kube-rbac-proxy front end
39+
openssl genrsa -out front-end.key
40+
openssl req -key front-end.key -new -out front-end.csr -subj '/CN=kube-rbac-proxy-front-end'
41+
openssl x509 -req -CA "${CA_CERT}" -CAkey "${CA_KEY}" -in front-end.csr -out front-end.crt -days 3650 -CAcreateserial \
42+
-extensions server \
43+
-extfile <(
44+
cat <<EOF
45+
[server]
46+
basicConstraints = CA:FALSE
47+
keyUsage = digitalSignature,keyEncipherment
48+
extendedKeyUsage = serverAuth
49+
subjectAltName=DNS:kube-rbac-proxy.default.svc.cluster.local
50+
EOF
51+
) 2> /dev/null
52+
53+
# Setup a certificate for the mock-server
54+
openssl genrsa -out mock-server.key
55+
openssl req -key mock-server.key -new -out mock-server.csr -subj '/CN=kube-rbac-proxy-mock-server'
56+
openssl x509 -req -CA "${CA_CERT}" -CAkey "${CA_KEY}" -in mock-server.csr -out mock-server.crt -days 3650 -CAcreateserial \
57+
-extensions server \
58+
-extfile <(
59+
cat <<EOF
60+
[server]
61+
basicConstraints = CA:FALSE
62+
keyUsage = digitalSignature,keyEncipherment
63+
extendedKeyUsage = serverAuth
64+
subjectAltName=DNS:mock-server.default.svc.cluster.local
65+
EOF
66+
) 2> /dev/null
67+
68+
# Setup a OAuth token signer
69+
openssl req -x509 -sha256 -days 3650 -newkey rsa:2048 -keyout oauth-token-signer.key -out oauth-token-signer.crt -noenc \
70+
-subj "/CN=kube-rbac-proxy-oauth-token-signer" \
71+
-addext "keyUsage=digitalSignature,keyEncipherment" > /dev/null 2>&1
72+
73+
# Clean up the serial number file
74+
rm -f ca.srl
75+
76+
# Create the device bundles
77+
cat client.crt "${CA_CERT}" > client-bundle.pem
78+
cat mock-server.crt "${CA_CERT}" > mock-server-bundle.pem
79+
cat front-end.crt "${CA_CERT}" > front-end-bundle.pem
80+
81+
# Create the Secret objects
82+
kubectl create secret generic -n default kube-rbac-proxy-client-certificates \
83+
--from-file=tls.crt=client-bundle.pem \
84+
--from-file=tls.key=client.key \
85+
--from-file=ca.crt="${CA_CERT}" \
86+
--dry-run=client -o yaml > client-certificate.yaml
87+
88+
kubectl create secret generic -n default kube-rbac-proxy-ca-certificate \
89+
--from-file=tls.crt="${CA_CERT}" \
90+
--from-file=tls.key="${CA_KEY}" \
91+
--dry-run=client -o yaml > ca-certificate.yaml
92+
93+
kubectl create secret generic -n default kube-rbac-proxy-mock-server-certificate \
94+
--from-file=tls.crt=mock-server-bundle.pem \
95+
--from-file=tls.key=mock-server.key \
96+
--dry-run=client -o yaml > mock-server-certificate.yaml
97+
98+
kubectl create secret generic -n default kube-rbac-proxy-front-end-certificate \
99+
--from-file=tls.crt=front-end-bundle.pem \
100+
--from-file=tls.key=front-end.key \
101+
--dry-run=client -o yaml > front-end-certificate.yaml
102+
103+
popd >/dev/null
104+
105+
# Distribute the certificates to the tests that require them
106+
cp "${WORKDIR}"/client-certificate.yaml "${E2E_DATA}/clientcertificates/certificate.yaml"
107+
cp "${WORKDIR}"/client-certificate.yaml "${E2E_DATA}/oidc/client-certificate.yaml"
108+
cp "${WORKDIR}"/ca-certificate.yaml "${E2E_DATA}/oidc/ca-certificate.yaml"
109+
cp "${WORKDIR}"/front-end-certificate.yaml "${E2E_DATA}/oidc/front-end-certificate.yaml"
110+
cp "${WORKDIR}"/mock-server-certificate.yaml "${E2E_DATA}/oidc/mock-server-certificate.yaml"
111+
112+
# Distribute other certificate data
113+
cp "${WORKDIR}/oauth-token-signer.crt" "${TEST_DATA}/"
114+
cp "${WORKDIR}/oauth-token-signer.key" "${TEST_DATA}/"
115+
116+
rm -rf -- "${WORKDIR}"
117+
118+
exit 0

scripts/generate-jwks.sh

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
#!/bin/bash
2+
3+
set -e
4+
5+
TOKEN_CERT=${1:-"./test/data/oauth-token-signer.crt"}
6+
TOKEN_KEYID=${2:-"0123456789abcdef"}
7+
JWKS_FILENAME=oauth-jwks.json
8+
WORKDIR=$(mktemp -d)
9+
10+
trap 'rm -rf -- "${WORKDIR}"' EXIT
11+
12+
if [ ! -d .git ]; then
13+
echo "This script must be run from the top-level directory of the repository."
14+
exit 1
15+
fi
16+
17+
if [ "$#" -lt 1 ]; then
18+
echo "Usage: generate-jwks.sh <token-cert> [<key-id>]"
19+
exit 1
20+
fi
21+
22+
TOKEN_CERT=$(realpath -s "${TOKEN_CERT}")
23+
24+
pushd "${WORKDIR}" >/dev/null
25+
26+
# URL encode a base64 string (see rfc4648)
27+
function url_encode_base64 {
28+
local VALUE=$1
29+
echo -n "${VALUE}" | tr -- '+/=' '-_ ' | sed -e 's/ //g'
30+
return 0
31+
}
32+
33+
# Extract the public key from the certificate
34+
openssl x509 -pubkey -noout -in "${TOKEN_CERT}" > token-cert.pub
35+
36+
# Extract the various JWKS parameters (see rfc7519)
37+
X5C=$(openssl x509 -in "${TOKEN_CERT}" -outform DER | base64 -w0)
38+
X5T=$(url_encode_base64 "$(openssl x509 -in "${TOKEN_CERT}" -outform DER | openssl sha1 -binary | base64 -w0)")
39+
X5T_S256=$(url_encode_base64 "$(openssl x509 -in "${TOKEN_CERT}" -outform DER | openssl sha256 -binary | base64 -w0)")
40+
N=$(url_encode_base64 "$(openssl rsa -pubin -in token-cert.pub -noout -modulus | sed -e 's/Modulus=//' | xxd -r -p | base64 -w0)")
41+
# The exponent ends up being the same for all of our certificates so we can use a static value
42+
E="AQAB"
43+
44+
# Build the main body
45+
JWKS=$(
46+
cat <<EOF
47+
{
48+
"kid": "${TOKEN_KEYID}",
49+
"kty": "RSA",
50+
"alg": "RS256",
51+
"use": "sig",
52+
"n": "${N}",
53+
"e": "${E}",
54+
"x5c": [
55+
"${X5C}"
56+
],
57+
"x5t": "${X5T}",
58+
"x5t#S256": "${X5T_S256}"
59+
}
60+
EOF
61+
)
62+
63+
# Clean/format up the JSON and save to a file
64+
echo "${JWKS}" | jq > ${JWKS_FILENAME}
65+
66+
popd >/dev/null
67+
68+
# Save the file for later
69+
cp "${WORKDIR}/${JWKS_FILENAME}" ./test/data/
70+
71+
rm -rf -- "${WORKDIR}"
72+
73+
exit 0

scripts/generate-tokens.sh

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
#!/bin/bash
2+
3+
set -e
4+
5+
KEY=${1:-"./test/data/oauth-token-signer.key"}
6+
KEYID=${2:-"0123456789abcdef"}
7+
SECRET_FILENAME="client-tokens-secret.yaml"
8+
TEST_CLIENT_ID="test-client-id"
9+
TEST_USERNAME="test-client"
10+
WORKDIR=$(mktemp -d)
11+
12+
trap 'rm -rf -- "${WORKDIR}"' EXIT
13+
14+
if [ ! -d .git ]; then
15+
echo "This script must be run from the top-level directory of the repository."
16+
exit 1
17+
fi
18+
19+
KEY=$(realpath -s "${KEY}")
20+
21+
pushd "${WORKDIR}" > /dev/null
22+
23+
# URL encode a base64 string (see rfc4648)
24+
function base64url_encode {
25+
local VALUE=$1
26+
echo -n "${VALUE}" | base64 -w0 | tr -- '+/=' '-_ ' | sed -e 's/ //g'
27+
return 0
28+
}
29+
30+
# Generate a token with a specific key-id, issued-at, and expired values
31+
function generate_token {
32+
local KEYID=$1
33+
local IAT=$2
34+
local EXP=$3
35+
local USERNAME=$4
36+
local CLIENT_ID=$5
37+
38+
# Construct the token header and footer (see rfc7519)
39+
JWT_HEADER=$(
40+
cat <<EOF
41+
{
42+
"typ":"JWT",
43+
"alg":"RS256",
44+
"kid":"${KEYID}"
45+
}
46+
EOF
47+
)
48+
49+
# username here aligns with the username assigned to the client in the client role binding
50+
JWT_BODY=$(
51+
cat <<EOF
52+
{
53+
"sub": "1234567890",
54+
"iss": "https://mock-server.default.svc.cluster.local:8443",
55+
"preferred_username": "${USERNAME}",
56+
"iat": ${IAT},
57+
"exp": ${EXP},
58+
"aud": "${CLIENT_ID}",
59+
"roles": ["metrics"]
60+
}
61+
EOF
62+
)
63+
64+
# Base64 encode the header and footer
65+
ENCODED_HEADER=$(base64url_encode "$(echo -n "${JWT_HEADER}" | jq -c .)")
66+
ENCODED_BODY=$(base64url_encode "$(echo -n "${JWT_BODY}" | jq -c .)")
67+
68+
# Sign the body of the token and generate the signature footer
69+
ENCODED_SIGNATURE=$(echo -n "${ENCODED_HEADER}.${ENCODED_BODY}" |
70+
openssl dgst -sha256 -sign "${KEY}" -binary |
71+
base64 -w0 | tr -- '+/=' '-_ ' | sed -e 's/ //g')
72+
73+
# Assemble the full token
74+
JWT_TOKEN="${ENCODED_HEADER}.${ENCODED_BODY}.${ENCODED_SIGNATURE}"
75+
76+
# Output the token to a file and then encapsulate that within a Secret.
77+
echo -n "${JWT_TOKEN}"
78+
}
79+
80+
NOW=$(date "+%s")
81+
generate_token "${KEYID}" "${NOW}" "$((NOW + 864000))" "${TEST_USERNAME}" "${TEST_CLIENT_ID}" > token.jwt
82+
generate_token "${KEYID}" "$((NOW - 86400))" "${NOW}" "${TEST_USERNAME}" "${TEST_CLIENT_ID}" > expired-token.jwt
83+
generate_token "unknown-key-id" "${NOW}" "$((NOW + 864000))" "${TEST_USERNAME}" "${TEST_CLIENT_ID}" > unknown-keyid-token.jwt
84+
generate_token "${KEYID}" "${NOW}" "$((NOW + 864000))" "unknown-username" "${TEST_CLIENT_ID}" > unknown-user-token.jwt
85+
generate_token "${KEYID}" "${NOW}" "$((NOW + 864000))" "${TEST_USERNAME}" "unknown-audience" > unknown-audience-token.jwt
86+
87+
kubectl create secret generic -n default kube-rbac-proxy-client-tokens \
88+
--from-file=token.jwt=token.jwt \
89+
--from-file=expired-token.jwt=expired-token.jwt \
90+
--from-file=unknown-token.jwt=unknown-keyid-token.jwt \
91+
--from-file=unknown-user-token.jwt=unknown-user-token.jwt \
92+
--from-file=unknown-audience-token.jwt=unknown-audience-token.jwt \
93+
--dry-run=client -oyaml > ${SECRET_FILENAME}
94+
95+
popd >/dev/null
96+
97+
# Distribute the file to the tests that require it.
98+
cp "${WORKDIR}/${SECRET_FILENAME}" ./test/e2e/oidc/
99+
100+
rm -rf -- "${WORKDIR}"
101+
102+
exit 0
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#!/bin/bash
2+
3+
set -e
4+
5+
if [ ! -d .git ]; then
6+
echo "This script must be run from the top-level directory of the repository."
7+
exit 1
8+
fi
9+
10+
EXPECTATION_FILE=./test/data/mock-server-expectations.json
11+
JWKS_FILE=./test/data/oauth-jwks.json
12+
MOCK_SERVER_CONFIGMAP=./test/e2e/oidc/mock-server-configmap.yaml
13+
14+
# Insert the JWKS fragment into the 2nd response for the /certs API endpoint
15+
mv "${EXPECTATION_FILE}" "${EXPECTATION_FILE}.orig"
16+
jq ".[1].httpResponse.body.keys.[0] = input" "${EXPECTATION_FILE}.orig" "${JWKS_FILE}" > "${EXPECTATION_FILE}"
17+
rm -f "${EXPECTATION_FILE}.orig"
18+
19+
# Create a configmap with the new expectations file
20+
kubectl create configmap -n default mock-server-config \
21+
--from-file=expectations.json="${EXPECTATION_FILE}" \
22+
--dry-run=client -o yaml > "${MOCK_SERVER_CONFIGMAP}"
23+
24+
exit 0

scripts/update-oidc-data.sh

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#!/bin/bash
2+
3+
set -e
4+
5+
KEYID=${1:-"0123456789abcdef"}
6+
7+
if [ ! -d .git ]; then
8+
echo "This script must be run from the top-level directory of the repository."
9+
exit 1
10+
fi
11+
12+
if [ "${1}" == "-f" ]; then
13+
# Only re-generate the certificates if the "-f" was specified since there is no need to do so unless they have
14+
# expired or their configuration attributes have been modified in some way.
15+
./scripts/generate-certificates.sh
16+
fi
17+
18+
# Generate the test tokens used by the client for various test scenarios
19+
./scripts/generate-tokens.sh ./test/data/oauth-token-signer.key "${KEYID}"
20+
21+
# Generate a JWKS profile to represent the public key to be used to verify the generated tokens
22+
./scripts/generate-jwks.sh ./test/data/oauth-token-signer.crt "${KEYID}"
23+
24+
# Inject the JWKS profile into the mock-server response configuration
25+
./scripts/update-mock-server-expectations.sh
26+
27+
exit 0

0 commit comments

Comments
 (0)