Using the certificates
Recipes for handing homepki material to Kubernetes, nginx, curl, OpenSSL, Java and Go. The examples use the runlocal.dev root, the bu1 intermediate, a kong-gateway server leaf and a my-client client leaf.
ROOT=${HOMEPKI_WORKDIR:-$HOME/.homepki}/runlocal-dev
CA_BUNDLE=$ROOT/bu1/bu1-intermediate-ca-chain.crt # intermediate + root
ROOT_CA=$ROOT/ca/runlocal-dev-root-ca.crt
SERVER_CRT=$ROOT/bu1/server-tls/kong-gateway.crt
SERVER_KEY=$ROOT/bu1/server-tls/kong-gateway.key
CLIENT_CRT=$ROOT/bu1/client-tls/my-client.crt
CLIENT_KEY=$ROOT/bu1/client-tls/my-client.key
Serve the leaf with its intermediate
Clients trust the root, not the intermediate, so a server must present its leaf followed by the intermediate. homepki writes no full-chain file; build one:
$ cat "$SERVER_CRT" "$ROOT/bu1/bu1-intermediate-ca.crt" > kong-gateway-fullchain.crt
A server that presents only its leaf fails with unable to get local issuer certificate (OpenSSL) or certificate signed by unknown authority (Go) on every client that trusts only the root.
Kubernetes
A TLS secret for an Ingress or a Gateway, with the chain, and a CA secret for services that verify client certificates:
$ kubectl create secret tls kong-gateway-tls \
--cert=kong-gateway-fullchain.crt --key="$SERVER_KEY"
$ kubectl create secret generic kong-ca \
--from-file=ca.crt="$CA_BUNDLE"
Keys are unencrypted, so they load without a passphrase. To have cert-manager issue leaves in the cluster, give its CA issuer an intermediate: kubectl create secret tls bu1-ca --cert=$ROOT/bu1/bu1-intermediate-ca.crt --key=$ROOT/bu1/private/bu1-intermediate-ca.key.
nginx
Server TLS
server {
listen 443 ssl;
server_name kong-gateway.bu1.runlocal.dev;
ssl_certificate /etc/nginx/tls/kong-gateway-fullchain.crt;
ssl_certificate_key /etc/nginx/tls/kong-gateway.key;
}
mTLS with revocation
Add client verification against the CA bundle. ssl_crl makes nginx check revocation on every CA in the chain, so it needs the CRL chain file, which holds a CRL for the intermediate and one for the root. Write it with homepki crl -d runlocal.dev -i bu1 before the first revocation.
ssl_client_certificate /etc/nginx/tls/bu1-intermediate-ca-chain.crt;
ssl_verify_client on;
ssl_verify_depth 2;
ssl_crl /etc/nginx/tls/bu1-crl-chain.crl;
After homepki revoke, copy the new bu1-crl-chain.crl and run nginx -s reload: nginx reads CRLs at startup and reload only. A CRL past its next update makes nginx reject every client certificate; re-run homepki crl before then. See Revocation.
curl
# server authentication: trust the root
$ curl --cacert "$ROOT_CA" https://kong-gateway.bu1.runlocal.dev/
# mTLS: also present the client certificate
$ curl --cacert "$ROOT_CA" --cert "$CLIENT_CRT" --key "$CLIENT_KEY" \
https://kong-gateway.bu1.runlocal.dev/
# the name does not resolve yet: pin it to an address
$ curl --cacert "$ROOT_CA" --resolve kong-gateway.bu1.runlocal.dev:443:127.0.0.1 \
https://kong-gateway.bu1.runlocal.dev/
After homepki trust install, curl builds that use the system store need no --cacert. A client certificate sent alone is verified by servers that trust the CA bundle, since the bundle contains the intermediate.
Check a server with OpenSSL
$ openssl s_client -connect 127.0.0.1:443 -servername kong-gateway.bu1.runlocal.dev \
-CAfile "$ROOT_CA" -verify_return_error -brief </dev/null
# with a client certificate
$ openssl s_client -connect 127.0.0.1:443 -servername kong-gateway.bu1.runlocal.dev \
-CAfile "$ROOT_CA" -cert "$CLIENT_CRT" -key "$CLIENT_KEY" -verify_return_error -brief </dev/null
-verify_return_error makes a broken chain fail the handshake instead of printing a warning. Verification: OK in the output means the server sent a chain that reaches the root.
To check files without a server:
$ openssl verify -CAfile "$CA_BUNDLE" "$SERVER_CRT"
$ openssl x509 -in "$SERVER_CRT" -noout -subject -ext subjectAltName,extendedKeyUsage
$ diff <(openssl x509 -in "$SERVER_CRT" -noout -pubkey) \
<(openssl pkey -in "$SERVER_KEY" -pubout) && echo "key matches"
Java
Issue the leaf with --pkcs12 to get a keystore holding the key, the leaf and the CA chain (password changeit unless --pkcs12-password is set). Build a truststore from the root:
$ homepki client-cert -d runlocal.dev -i bu1 -c my-client --pkcs12
$ keytool -importcert -noprompt -alias runlocal-dev-root \
-file "$ROOT_CA" -keystore truststore.p12 -storetype PKCS12 -storepass changeit
$ java -Djavax.net.ssl.keyStore=$ROOT/bu1/client-tls/my-client.p12 \
-Djavax.net.ssl.keyStorePassword=changeit -Djavax.net.ssl.keyStoreType=PKCS12 \
-Djavax.net.ssl.trustStore=truststore.p12 \
-Djavax.net.ssl.trustStorePassword=changeit -Djavax.net.ssl.trustStoreType=PKCS12 \
-jar app.jar
To trust the root in every Java program of a JDK instead, run homepki trust install -d runlocal.dev --store java with JAVA_HOME set.
Go
A Go HTTP client for mTLS. pki.LoadCerts reads both certificates of the chain file, intermediate first:
import (
"crypto/tls"
"crypto/x509"
"net/http"
"github.com/bcollard/homepki/pkg/pki"
)
func client(dir string) (*http.Client, error) {
cas, err := pki.LoadCerts(dir + "/bu1/bu1-intermediate-ca-chain.crt")
if err != nil {
return nil, err
}
roots := x509.NewCertPool()
roots.AddCert(cas[len(cas)-1]) // the root is last in the chain file
pair, err := tls.LoadX509KeyPair(dir+"/bu1/client-tls/my-client.crt", dir+"/bu1/client-tls/my-client.key")
if err != nil {
return nil, err
}
pair.Certificate = append(pair.Certificate, cas[0].Raw) // send the intermediate too
return &http.Client{Transport: &http.Transport{TLSClientConfig: &tls.Config{
RootCAs: roots,
Certificates: []tls.Certificate{pair},
}}}, nil
}
Without the pkg/pki import, x509.CertPool.AppendCertsFromPEM on the chain file works too; it adds both CAs as trust anchors.