Created
September 20, 2025 12:53
-
-
Save hiyosi/0bbe68c2f3a5de37ab36e5a86f7887a2 to your computer and use it in GitHub Desktop.
PodCertificateRequest Issuer
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| set -euo pipefail | |
| # Configuration | |
| NAMESPACE="${NAMESPACE:-default}" | |
| PCR_NAME="${PCR_NAME:-}" | |
| CA_KEY="${CA_KEY:-ca.key}" | |
| CA_CRT="${CA_CRT:-ca.crt}" | |
| POD_NAME="${POD_NAME:-pod-cert-test}" | |
| CREATE_POD="${CREATE_POD:-false}" | |
| # Colors for output | |
| RED='\033[0;31m' | |
| GREEN='\033[0;32m' | |
| YELLOW='\033[1;33m' | |
| NC='\033[0m' # No Color | |
| log() { | |
| echo -e "${GREEN}[$(date '+%Y-%m-%d %H:%M:%S')] $1${NC}" | |
| } | |
| warn() { | |
| echo -e "${YELLOW}[$(date '+%Y-%m-%d %H:%M:%S')] WARNING: $1${NC}" | |
| } | |
| error() { | |
| echo -e "${RED}[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: $1${NC}" | |
| exit 1 | |
| } | |
| # Show usage | |
| show_usage() { | |
| cat <<EOF | |
| Pod Certificate Issuer Script | |
| Usage: $0 [OPTIONS] | |
| Options: | |
| -h, --help Show this help message | |
| -c, --create-pod Create a Pod with podCertificate volume | |
| -p, --pod-name NAME Pod name (default: pod-cert-test) | |
| -n, --namespace NAME Namespace (default: default) | |
| --pcr-name NAME PodCertificateRequest name (auto-detect if not specified) | |
| --ca-key FILE CA private key file (default: ca.key) | |
| --ca-crt FILE CA certificate file (default: ca.crt) | |
| Environment Variables: | |
| NAMESPACE Target namespace (default: default) | |
| PCR_NAME PodCertificateRequest name | |
| POD_NAME Pod name (default: pod-cert-test) | |
| CREATE_POD Create pod if true (default: false) | |
| CA_KEY CA private key file (default: ca.key) | |
| CA_CRT CA certificate file (default: ca.crt) | |
| Examples: | |
| # Issue certificate for existing PodCertificateRequest | |
| $0 | |
| # Create Pod and issue certificate | |
| $0 --create-pod | |
| # Create Pod with custom name and issue certificate | |
| $0 --create-pod --pod-name my-test-pod | |
| # Specify namespace and PCR name | |
| $0 --namespace kube-system --pcr-name req-12345 | |
| EOF | |
| } | |
| # Check prerequisites | |
| check_prerequisites() { | |
| log "Checking prerequisites..." | |
| # Check required commands | |
| for cmd in kubectl openssl jq gdate; do | |
| if ! command -v $cmd &> /dev/null; then | |
| error "$cmd is required but not installed" | |
| fi | |
| done | |
| # Check CA files | |
| if [[ ! -f "$CA_KEY" ]]; then | |
| error "CA private key not found: $CA_KEY" | |
| fi | |
| if [[ ! -f "$CA_CRT" ]]; then | |
| error "CA certificate not found: $CA_CRT" | |
| fi | |
| # Check OpenSSL version for -force_pubkey support | |
| if ! openssl x509 -help 2>&1 | grep -q "force_pubkey"; then | |
| error "OpenSSL version does not support -force_pubkey option" | |
| fi | |
| log "Prerequisites check passed" | |
| } | |
| # Create Pod if requested | |
| create_pod_if_needed() { | |
| if [[ "$CREATE_POD" == "true" ]]; then | |
| log "Creating Pod with podCertificate volume..." | |
| # Check if pod already exists | |
| if kubectl get pod "$POD_NAME" -n "$NAMESPACE" &>/dev/null; then | |
| log "Pod $POD_NAME already exists in namespace $NAMESPACE" | |
| return 0 | |
| fi | |
| # Create pod YAML | |
| cat > pod-with-cert.yaml <<EOF | |
| apiVersion: v1 | |
| kind: Pod | |
| metadata: | |
| name: $POD_NAME | |
| namespace: $NAMESPACE | |
| spec: | |
| containers: | |
| - name: test-container | |
| image: alpine:3.18 | |
| command: ["/bin/sh"] | |
| args: ["-c", "while true; do echo 'Waiting for certificates...'; ls -la /etc/certs/; sleep 30; done"] | |
| volumeMounts: | |
| - name: pod-certs | |
| mountPath: /etc/certs | |
| readOnly: true | |
| volumes: | |
| - name: pod-certs | |
| projected: | |
| sources: | |
| - podCertificate: | |
| signerName: signer-hiyosi | |
| keyType: ECDSAP256 | |
| maxExpirationSeconds: 86400 | |
| credentialBundlePath: credential-bundle.pem | |
| EOF | |
| # Apply the pod | |
| kubectl apply -f pod-with-cert.yaml | |
| log "Pod $POD_NAME created successfully" | |
| fi | |
| } | |
| # Find PodCertificateRequest | |
| find_pcr() { | |
| if [[ -z "$PCR_NAME" ]]; then | |
| log "Finding PodCertificateRequest in namespace $NAMESPACE..." | |
| # Wait for PodCertificateRequest to be created (max 60 seconds) | |
| local max_wait=60 | |
| local wait_time=0 | |
| while [[ $wait_time -lt $max_wait ]]; do | |
| PCR_NAME=$(kubectl get podcertificaterequest -n "$NAMESPACE" --no-headers -o custom-columns=":metadata.name" 2>/dev/null | head -1) | |
| if [[ -n "$PCR_NAME" ]]; then | |
| log "Found PodCertificateRequest: $PCR_NAME" | |
| return 0 | |
| fi | |
| if [[ $wait_time -eq 0 ]]; then | |
| log "Waiting for PodCertificateRequest to be created..." | |
| fi | |
| sleep 5 | |
| wait_time=$((wait_time + 5)) | |
| echo -n "." | |
| done | |
| echo # New line after dots | |
| error "No PodCertificateRequest found in namespace $NAMESPACE after ${max_wait}s" | |
| fi | |
| } | |
| # Extract PCR information | |
| extract_pcr_info() { | |
| log "Extracting PodCertificateRequest information..." | |
| kubectl get podcertificaterequest "$PCR_NAME" -n "$NAMESPACE" -o json > pcr.json | |
| PUBLIC_KEY_B64=$(jq -r '.spec.pkixPublicKey' pcr.json) | |
| POD_NAME=$(jq -r '.spec.podName' pcr.json) | |
| POD_UID=$(jq -r '.spec.podUID' pcr.json) | |
| NODE_NAME=$(jq -r '.spec.nodeName' pcr.json) | |
| NODE_UID=$(jq -r '.spec.nodeUID' pcr.json) | |
| SERVICE_ACCOUNT=$(jq -r '.spec.serviceAccountName' pcr.json) | |
| SERVICE_ACCOUNT_UID=$(jq -r '.spec.serviceAccountUID' pcr.json) | |
| MAX_EXPIRATION_SECONDS=$(jq -r '.spec.maxExpirationSeconds // 86400' pcr.json) | |
| NAMESPACE_UID=$(kubectl get namespace "$NAMESPACE" -o jsonpath='{.metadata.uid}') | |
| log "Pod: $POD_NAME, Service Account: $SERVICE_ACCOUNT, Max Expiration: ${MAX_EXPIRATION_SECONDS}s" | |
| } | |
| # Convert public key | |
| convert_public_key() { | |
| log "Converting public key..." | |
| echo "$PUBLIC_KEY_B64" | base64 -d > pod-public.der | |
| openssl ec -pubin -inform DER -in pod-public.der -outform PEM -out pod-public.pem | |
| log "Public key converted to PEM format" | |
| } | |
| # Create certificate configuration | |
| create_cert_config() { | |
| log "Creating certificate configuration..." | |
| # Certificate subject configuration | |
| cat > cert.cnf <<EOF | |
| [req] | |
| distinguished_name = req_dn | |
| prompt = no | |
| [req_dn] | |
| CN = system:pod:${NAMESPACE}:${POD_NAME} | |
| O = system:pods | |
| EOF | |
| # Certificate extensions configuration | |
| cat > ext.cnf <<EOF | |
| basicConstraints = CA:FALSE | |
| keyUsage = critical,digitalSignature,keyEncipherment | |
| extendedKeyUsage = clientAuth | |
| subjectKeyIdentifier = hash | |
| authorityKeyIdentifier = keyid:always | |
| # Subject Alternative Names | |
| subjectAltName = @alt_names | |
| # Pod Identity Extension - Official Kubernetes OID (KEP-4317) | |
| 1.3.6.1.4.1.57683.1 = critical,ASN1:SEQUENCE:pod_identity_section | |
| [alt_names] | |
| DNS.1 = ${POD_NAME} | |
| DNS.2 = ${POD_NAME}.${NAMESPACE} | |
| DNS.3 = ${POD_NAME}.${NAMESPACE}.pod.cluster.local | |
| [pod_identity_section] | |
| # Fields with implicit tags as per KEP-4317 specification | |
| field0 = IMP:0,UTF8:${NAMESPACE} | |
| field1 = IMP:1,UTF8:${NAMESPACE_UID} | |
| field2 = IMP:2,UTF8:${SERVICE_ACCOUNT} | |
| field3 = IMP:3,UTF8:${SERVICE_ACCOUNT_UID} | |
| field4 = IMP:4,UTF8:${POD_NAME} | |
| field5 = IMP:5,UTF8:${POD_UID} | |
| field6 = IMP:6,UTF8:${NODE_NAME} | |
| field7 = IMP:7,UTF8:${NODE_UID} | |
| EOF | |
| log "Certificate configuration created" | |
| } | |
| # Issue certificate | |
| issue_certificate() { | |
| log "Issuing certificate..." | |
| # Create temporary private key for CSR (will be replaced) | |
| openssl ecparam -genkey -name prime256v1 -out temp-private.pem | |
| # Create CSR with temporary key | |
| openssl req -new -key temp-private.pem -out temp.csr -config cert.cnf | |
| # Calculate certificate validity | |
| DAYS=$((MAX_EXPIRATION_SECONDS / 86400)) | |
| if [[ $DAYS -eq 0 ]]; then | |
| DAYS=1 | |
| fi | |
| # Issue certificate with actual public key | |
| openssl x509 -req -in temp.csr -CA "$CA_CRT" -CAkey "$CA_KEY" -CAcreateserial \ | |
| -out pod-cert.crt -days $DAYS -extfile ext.cnf \ | |
| -force_pubkey pod-public.pem | |
| log "Certificate issued successfully" | |
| } | |
| # Create credential bundle | |
| create_credential_bundle() { | |
| log "Creating credential bundle..." | |
| cat pod-cert.crt ca.crt > credential-bundle.pem | |
| log "Credential bundle created" | |
| } | |
| # Update PodCertificateRequest status | |
| update_pcr_status() { | |
| log "Updating PodCertificateRequest status..." | |
| # Extract certificate dates | |
| NOT_BEFORE=$(openssl x509 -in pod-cert.crt -noout -startdate | cut -d= -f2) | |
| NOT_BEFORE=$(gdate -u -d "$NOT_BEFORE" +"%Y-%m-%dT%H:%M:%SZ") | |
| # Calculate notAfter | |
| NOT_BEFORE_EPOCH=$(gdate -u -d "$NOT_BEFORE" +%s) | |
| NOT_AFTER=$(gdate -u -d "@$((NOT_BEFORE_EPOCH + MAX_EXPIRATION_SECONDS))" +"%Y-%m-%dT%H:%M:%SZ") | |
| # Calculate refresh time (80% of validity period) | |
| REFRESH_SECONDS=$((MAX_EXPIRATION_SECONDS * 80 / 100)) | |
| BEGIN_REFRESH_AT=$(gdate -u -d "@$((NOT_BEFORE_EPOCH + REFRESH_SECONDS))" +"%Y-%m-%dT%H:%M:%SZ") | |
| # Convert certificate chain to JSON string | |
| CREDENTIAL_BUNDLE=$(cat credential-bundle.pem | jq -Rs '.') | |
| # Create patch JSON | |
| cat > patch.json <<EOF | |
| { | |
| "status": { | |
| "certificateChain": $CREDENTIAL_BUNDLE, | |
| "notBefore": "$NOT_BEFORE", | |
| "notAfter": "$NOT_AFTER", | |
| "beginRefreshAt": "$BEGIN_REFRESH_AT", | |
| "conditions": [ | |
| { | |
| "type": "Issued", | |
| "status": "True", | |
| "reason": "Issued", | |
| "message": "Certificate issued with KEP-4317 Pod Identity extension", | |
| "lastTransitionTime": "$NOT_BEFORE" | |
| } | |
| ] | |
| } | |
| } | |
| EOF | |
| # Apply patch | |
| kubectl patch podcertificaterequest "$PCR_NAME" -n "$NAMESPACE" \ | |
| --type=merge --subresource=status -p "$(cat patch.json)" | |
| log "PodCertificateRequest status updated successfully" | |
| } | |
| # Verify certificate | |
| verify_certificate() { | |
| log "Verifying certificate..." | |
| # Check certificate validity | |
| openssl x509 -in pod-cert.crt -noout -text | grep -A 5 "Validity" | |
| # Verify public key match | |
| PCR_PUBKEY=$(openssl ec -pubin -in pod-public.pem -text -noout | grep -A 10 "pub:" | grep -E "^\s*[0-9a-f]{2}:" | tr -d ' :' | tr -d '\n') | |
| CERT_PUBKEY=$(openssl x509 -in pod-cert.crt -pubkey -noout | openssl ec -pubin -text -noout | grep -A 10 "pub:" | grep -E "^\s*[0-9a-f]{2}:" | tr -d ' :' | tr -d '\n') | |
| if [[ "$PCR_PUBKEY" == "$CERT_PUBKEY" ]]; then | |
| log "Public key verification: PASSED" | |
| else | |
| error "Public key verification: FAILED" | |
| fi | |
| # Check Pod Identity extension | |
| if openssl x509 -in pod-cert.crt -text -noout | grep -q "1.3.6.1.4.1.57683.1"; then | |
| log "Pod Identity extension: PRESENT" | |
| else | |
| warn "Pod Identity extension: NOT FOUND" | |
| fi | |
| } | |
| # Cleanup temporary files | |
| cleanup() { | |
| log "Cleaning up temporary files..." | |
| rm -f temp-private.pem temp.csr pod-public.der cert.cnf ext.cnf pcr.json | |
| log "Cleanup completed" | |
| } | |
| # Parse command line arguments | |
| parse_args() { | |
| while [[ $# -gt 0 ]]; do | |
| case $1 in | |
| -h|--help) | |
| show_usage | |
| exit 0 | |
| ;; | |
| -c|--create-pod) | |
| CREATE_POD="true" | |
| shift | |
| ;; | |
| -p|--pod-name) | |
| POD_NAME="$2" | |
| shift 2 | |
| ;; | |
| -n|--namespace) | |
| NAMESPACE="$2" | |
| shift 2 | |
| ;; | |
| --pcr-name) | |
| PCR_NAME="$2" | |
| shift 2 | |
| ;; | |
| --ca-key) | |
| CA_KEY="$2" | |
| shift 2 | |
| ;; | |
| --ca-crt) | |
| CA_CRT="$2" | |
| shift 2 | |
| ;; | |
| *) | |
| error "Unknown option: $1" | |
| ;; | |
| esac | |
| done | |
| } | |
| # Main execution | |
| main() { | |
| parse_args "$@" | |
| log "Starting Pod Certificate Issuer" | |
| check_prerequisites | |
| create_pod_if_needed | |
| find_pcr | |
| extract_pcr_info | |
| convert_public_key | |
| create_cert_config | |
| issue_certificate | |
| create_credential_bundle | |
| update_pcr_status | |
| verify_certificate | |
| cleanup | |
| log "Certificate issuance completed successfully!" | |
| log "PodCertificateRequest: $PCR_NAME" | |
| log "Certificate files created: pod-cert.crt, credential-bundle.pem" | |
| # Show final status | |
| kubectl get podcertificaterequest "$PCR_NAME" -n "$NAMESPACE" | |
| } | |
| # Run main function | |
| main "$@" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment