Skip to content

Commit

Permalink
Add E2E tests for OIDC token validation
Browse files Browse the repository at this point in the history
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]>
  • Loading branch information
alegacy committed Nov 13, 2024
1 parent b76470f commit 042bd58
Show file tree
Hide file tree
Showing 33 changed files with 1,196 additions and 6 deletions.
7 changes: 6 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ PKGS=$(shell go list ./... | grep -v /test/e2e)
DOCKER_REPO?=quay.io/brancz/kube-rbac-proxy
KUBECONFIG?=$(HOME)/.kube/config
CONTAINER_NAME?=$(DOCKER_REPO):$(VERSION)
KRP_CURL_OIDC_OVERRIDES=$(shell cat ./test/data/krp-curl-oidc-overrides.json)

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

run-curl-container:
@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'
kubectl run -i -t krp-curl --image=quay.io/brancz/krp-curl:v0.0.2 --restart=Never --command -- /bin/sh
kubectl run -i -t --rm krp-curl --image=quay.io/brancz/krp-curl:v0.0.2 --restart=Never --command -- /bin/sh

run-oidc-curl-container:
@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'
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

grpcc-container:
docker build -f ./examples/grpcc/Dockerfile -t mumoshu/grpcc:v0.0.1 .
Expand Down
118 changes: 118 additions & 0 deletions scripts/generate-certificates.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
#!/bin/bash

set -e

E2E_DATA=./test/e2e
TEST_DATA=./test/data
WORKDIR=$(mktemp -d)

trap 'rm -rf -- "${WORKDIR}"' EXIT

if [ ! -d .git ]; then
echo "This script must be run from the top-level directory of the repository."
exit 1
fi

pushd "${WORKDIR}" > /dev/null

# Setup a certificate for our test CA
openssl req -x509 -sha256 -days 3650 -newkey rsa:2048 -keyout ca.key -out ca.crt -noenc \
-subj "/CN=kube-rbac-proxy-signer" \
-addext "keyUsage=digitalSignature,keyEncipherment,cRLSign,keyCertSign" > /dev/null 2>&1
CA_CERT=ca.crt
CA_KEY=ca.key

# Setup a certificate for the test client
openssl genrsa -out client.key 2048
openssl req -key client.key -new -out client.csr -subj '/CN=kube-rbac-proxy-certificates-test'
openssl x509 -req -CA "${CA_CERT}" -CAkey "${CA_KEY}" -in client.csr -out client.crt -days 3650 -CAcreateserial \
-extensions client \
-extfile <(
cat <<EOF
[client]
basicConstraints = CA:FALSE
extendedKeyUsage = clientAuth
EOF
) 2> /dev/null

# Setup a certificate for the kube-rbac-proxy front end
openssl genrsa -out front-end.key
openssl req -key front-end.key -new -out front-end.csr -subj '/CN=kube-rbac-proxy-front-end'
openssl x509 -req -CA "${CA_CERT}" -CAkey "${CA_KEY}" -in front-end.csr -out front-end.crt -days 3650 -CAcreateserial \
-extensions server \
-extfile <(
cat <<EOF
[server]
basicConstraints = CA:FALSE
keyUsage = digitalSignature,keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName=DNS:kube-rbac-proxy.default.svc.cluster.local
EOF
) 2> /dev/null

# Setup a certificate for the mock-server
openssl genrsa -out mock-server.key
openssl req -key mock-server.key -new -out mock-server.csr -subj '/CN=kube-rbac-proxy-mock-server'
openssl x509 -req -CA "${CA_CERT}" -CAkey "${CA_KEY}" -in mock-server.csr -out mock-server.crt -days 3650 -CAcreateserial \
-extensions server \
-extfile <(
cat <<EOF
[server]
basicConstraints = CA:FALSE
keyUsage = digitalSignature,keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName=DNS:mock-server.default.svc.cluster.local
EOF
) 2> /dev/null

# Setup a OAuth token signer
openssl req -x509 -sha256 -days 3650 -newkey rsa:2048 -keyout oauth-token-signer.key -out oauth-token-signer.crt -noenc \
-subj "/CN=kube-rbac-proxy-oauth-token-signer" \
-addext "keyUsage=digitalSignature,keyEncipherment" > /dev/null 2>&1

# Clean up the serial number file
rm -f ca.srl

# Create the device bundles
cat client.crt "${CA_CERT}" > client-bundle.pem
cat mock-server.crt "${CA_CERT}" > mock-server-bundle.pem
cat front-end.crt "${CA_CERT}" > front-end-bundle.pem

# Create the Secret objects
kubectl create secret generic -n default kube-rbac-proxy-client-certificates \
--from-file=tls.crt=client-bundle.pem \
--from-file=tls.key=client.key \
--from-file=ca.crt="${CA_CERT}" \
--dry-run=client -o yaml > client-certificate.yaml

kubectl create secret generic -n default kube-rbac-proxy-ca-certificate \
--from-file=tls.crt="${CA_CERT}" \
--from-file=tls.key="${CA_KEY}" \
--dry-run=client -o yaml > ca-certificate.yaml

kubectl create secret generic -n default kube-rbac-proxy-mock-server-certificate \
--from-file=tls.crt=mock-server-bundle.pem \
--from-file=tls.key=mock-server.key \
--dry-run=client -o yaml > mock-server-certificate.yaml

kubectl create secret generic -n default kube-rbac-proxy-front-end-certificate \
--from-file=tls.crt=front-end-bundle.pem \
--from-file=tls.key=front-end.key \
--dry-run=client -o yaml > front-end-certificate.yaml

popd >/dev/null

# Distribute the certificates to the tests that require them
cp "${WORKDIR}"/client-certificate.yaml "${E2E_DATA}/clientcertificates/certificate.yaml"
cp "${WORKDIR}"/client-certificate.yaml "${E2E_DATA}/oidc/client-certificate.yaml"
cp "${WORKDIR}"/ca-certificate.yaml "${E2E_DATA}/oidc/ca-certificate.yaml"
cp "${WORKDIR}"/front-end-certificate.yaml "${E2E_DATA}/oidc/front-end-certificate.yaml"
cp "${WORKDIR}"/mock-server-certificate.yaml "${E2E_DATA}/oidc/mock-server-certificate.yaml"

# Distribute other certificate data
cp "${WORKDIR}/oauth-token-signer.crt" "${TEST_DATA}/"
cp "${WORKDIR}/oauth-token-signer.key" "${TEST_DATA}/"

rm -rf -- "${WORKDIR}"

exit 0
73 changes: 73 additions & 0 deletions scripts/generate-jwks.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#!/bin/bash

set -e

TOKEN_CERT=${1:-"./test/data/oauth-token-signer.crt"}
TOKEN_KEYID=${2:-"0123456789abcdef"}
JWKS_FILENAME=oauth-jwks.json
WORKDIR=$(mktemp -d)

trap 'rm -rf -- "${WORKDIR}"' EXIT

if [ ! -d .git ]; then
echo "This script must be run from the top-level directory of the repository."
exit 1
fi

if [ "$#" -lt 1 ]; then
echo "Usage: generate-jwks.sh <token-cert> [<key-id>]"
exit 1
fi

TOKEN_CERT=$(realpath -s "${TOKEN_CERT}")

pushd "${WORKDIR}" >/dev/null

# URL encode a base64 string (see rfc4648)
function url_encode_base64 {
local VALUE=$1
echo -n "${VALUE}" | tr -- '+/=' '-_ ' | sed -e 's/ //g'
return 0
}

# Extract the public key from the certificate
openssl x509 -pubkey -noout -in "${TOKEN_CERT}" > token-cert.pub

# Extract the various JWKS parameters (see rfc7519)
X5C=$(openssl x509 -in "${TOKEN_CERT}" -outform DER | base64 -w0)
X5T=$(url_encode_base64 "$(openssl x509 -in "${TOKEN_CERT}" -outform DER | openssl sha1 -binary | base64 -w0)")
X5T_S256=$(url_encode_base64 "$(openssl x509 -in "${TOKEN_CERT}" -outform DER | openssl sha256 -binary | base64 -w0)")
N=$(url_encode_base64 "$(openssl rsa -pubin -in token-cert.pub -noout -modulus | sed -e 's/Modulus=//' | xxd -r -p | base64 -w0)")
# The exponent ends up being the same for all of our certificates so we can use a static value
E="AQAB"

# Build the main body
JWKS=$(
cat <<EOF
{
"kid": "${TOKEN_KEYID}",
"kty": "RSA",
"alg": "RS256",
"use": "sig",
"n": "${N}",
"e": "${E}",
"x5c": [
"${X5C}"
],
"x5t": "${X5T}",
"x5t#S256": "${X5T_S256}"
}
EOF
)

# Clean/format up the JSON and save to a file
echo "${JWKS}" | jq > ${JWKS_FILENAME}

popd >/dev/null

# Save the file for later
cp "${WORKDIR}/${JWKS_FILENAME}" ./test/data/

rm -rf -- "${WORKDIR}"

exit 0
102 changes: 102 additions & 0 deletions scripts/generate-tokens.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
#!/bin/bash

set -e

KEY=${1:-"./test/data/oauth-token-signer.key"}
KEYID=${2:-"0123456789abcdef"}
SECRET_FILENAME="client-tokens-secret.yaml"
TEST_CLIENT_ID="test-client-id"
TEST_USERNAME="test-client"
WORKDIR=$(mktemp -d)

trap 'rm -rf -- "${WORKDIR}"' EXIT

if [ ! -d .git ]; then
echo "This script must be run from the top-level directory of the repository."
exit 1
fi

KEY=$(realpath -s "${KEY}")

pushd "${WORKDIR}" > /dev/null

# URL encode a base64 string (see rfc4648)
function base64url_encode {
local VALUE=$1
echo -n "${VALUE}" | base64 -w0 | tr -- '+/=' '-_ ' | sed -e 's/ //g'
return 0
}

# Generate a token with a specific key-id, issued-at, and expired values
function generate_token {
local KEYID=$1
local IAT=$2
local EXP=$3
local USERNAME=$4
local CLIENT_ID=$5

# Construct the token header and footer (see rfc7519)
JWT_HEADER=$(
cat <<EOF
{
"typ":"JWT",
"alg":"RS256",
"kid":"${KEYID}"
}
EOF
)

# username here aligns with the username assigned to the client in the client role binding
JWT_BODY=$(
cat <<EOF
{
"sub": "1234567890",
"iss": "https://mock-server.default.svc.cluster.local:8443",
"preferred_username": "${USERNAME}",
"iat": ${IAT},
"exp": ${EXP},
"aud": "${CLIENT_ID}",
"roles": ["metrics"]
}
EOF
)

# Base64 encode the header and footer
ENCODED_HEADER=$(base64url_encode "$(echo -n "${JWT_HEADER}" | jq -c .)")
ENCODED_BODY=$(base64url_encode "$(echo -n "${JWT_BODY}" | jq -c .)")

# Sign the body of the token and generate the signature footer
ENCODED_SIGNATURE=$(echo -n "${ENCODED_HEADER}.${ENCODED_BODY}" |
openssl dgst -sha256 -sign "${KEY}" -binary |
base64 -w0 | tr -- '+/=' '-_ ' | sed -e 's/ //g')

# Assemble the full token
JWT_TOKEN="${ENCODED_HEADER}.${ENCODED_BODY}.${ENCODED_SIGNATURE}"

# Output the token to a file and then encapsulate that within a Secret.
echo -n "${JWT_TOKEN}"
}

NOW=$(date "+%s")
generate_token "${KEYID}" "${NOW}" "$((NOW + 864000))" "${TEST_USERNAME}" "${TEST_CLIENT_ID}" > token.jwt
generate_token "${KEYID}" "$((NOW - 86400))" "${NOW}" "${TEST_USERNAME}" "${TEST_CLIENT_ID}" > expired-token.jwt
generate_token "unknown-key-id" "${NOW}" "$((NOW + 864000))" "${TEST_USERNAME}" "${TEST_CLIENT_ID}" > unknown-keyid-token.jwt
generate_token "${KEYID}" "${NOW}" "$((NOW + 864000))" "unknown-username" "${TEST_CLIENT_ID}" > unknown-user-token.jwt
generate_token "${KEYID}" "${NOW}" "$((NOW + 864000))" "${TEST_USERNAME}" "unknown-audience" > unknown-audience-token.jwt

kubectl create secret generic -n default kube-rbac-proxy-client-tokens \
--from-file=token.jwt=token.jwt \
--from-file=expired-token.jwt=expired-token.jwt \
--from-file=unknown-token.jwt=unknown-keyid-token.jwt \
--from-file=unknown-user-token.jwt=unknown-user-token.jwt \
--from-file=unknown-audience-token.jwt=unknown-audience-token.jwt \
--dry-run=client -oyaml > ${SECRET_FILENAME}

popd >/dev/null

# Distribute the file to the tests that require it.
cp "${WORKDIR}/${SECRET_FILENAME}" ./test/e2e/oidc/

rm -rf -- "${WORKDIR}"

exit 0
24 changes: 24 additions & 0 deletions scripts/update-mock-server-expectations.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/bin/bash

set -e

if [ ! -d .git ]; then
echo "This script must be run from the top-level directory of the repository."
exit 1
fi

EXPECTATION_FILE=./test/data/mock-server-expectations.json
JWKS_FILE=./test/data/oauth-jwks.json
MOCK_SERVER_CONFIGMAP=./test/e2e/oidc/mock-server-configmap.yaml

# Insert the JWKS fragment into the 2nd response for the /certs API endpoint
mv "${EXPECTATION_FILE}" "${EXPECTATION_FILE}.orig"
jq ".[1].httpResponse.body.keys.[0] = input" "${EXPECTATION_FILE}.orig" "${JWKS_FILE}" > "${EXPECTATION_FILE}"
rm -f "${EXPECTATION_FILE}.orig"

# Create a configmap with the new expectations file
kubectl create configmap -n default mock-server-config \
--from-file=expectations.json="${EXPECTATION_FILE}" \
--dry-run=client -o yaml > "${MOCK_SERVER_CONFIGMAP}"

exit 0
27 changes: 27 additions & 0 deletions scripts/update-oidc-data.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/bin/bash

set -e

KEYID=${1:-"0123456789abcdef"}

if [ ! -d .git ]; then
echo "This script must be run from the top-level directory of the repository."
exit 1
fi

if [ "${1}" == "-f" ]; then
# Only re-generate the certificates if the "-f" was specified since there is no need to do so unless they have
# expired or their configuration attributes have been modified in some way.
./scripts/generate-certificates.sh
fi

# Generate the test tokens used by the client for various test scenarios
./scripts/generate-tokens.sh ./test/data/oauth-token-signer.key "${KEYID}"

# Generate a JWKS profile to represent the public key to be used to verify the generated tokens
./scripts/generate-jwks.sh ./test/data/oauth-token-signer.crt "${KEYID}"

# Inject the JWKS profile into the mock-server response configuration
./scripts/update-mock-server-expectations.sh

exit 0
Loading

0 comments on commit 042bd58

Please sign in to comment.