forked from TrueCloudLab/certificates
hello-mtls examples
This commit is contained in:
parent
8e1505d03f
commit
f58000c28f
10 changed files with 389 additions and 0 deletions
55
autocert/examples/hello-mtls/README.md
Normal file
55
autocert/examples/hello-mtls/README.md
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
# hello-mtls
|
||||||
|
|
||||||
|
This repository contains examples of dockerized [m]TLS clients and servers in
|
||||||
|
various languages. There's a lot of confusion and misinformation regarding how
|
||||||
|
to do mTLS properly with an internal public key infrastructure. The goal of
|
||||||
|
this repository is to demonstrate best practices like:
|
||||||
|
|
||||||
|
* Properly configuring TLS to use your internal CA's root certificate
|
||||||
|
* mTLS (client certificates / client authentication)
|
||||||
|
* Short-lived certificate support (clients and servers automatically load
|
||||||
|
renewed certificates)
|
||||||
|
|
||||||
|
Examples use multi-stage docker builds and can be built via without any
|
||||||
|
required local dependencies (except `docker`):
|
||||||
|
|
||||||
|
```
|
||||||
|
docker build -f Dockerfile.server -t hello-mtls-server-<lang> .
|
||||||
|
docker build -f Dockerfile.client -t hello-mtls-client-<lang> .
|
||||||
|
```
|
||||||
|
|
||||||
|
Once built, you should be able to deploy via:
|
||||||
|
|
||||||
|
```
|
||||||
|
kubectl apply -f hello-mtls.server.yaml
|
||||||
|
kubectl apply -f hello-mtls.client.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Feature matrix
|
||||||
|
|
||||||
|
This matrix shows the set of features we'd like to demonstrate in each language
|
||||||
|
and where each language is. Bug fixes, improvements, and examples in new
|
||||||
|
languages are appreciated!
|
||||||
|
|
||||||
|
[go/](go/)
|
||||||
|
- [X] Server using autocert certificate & key
|
||||||
|
- [X] mTLS (client authentication using internal root certificate)
|
||||||
|
- [X] Automatic certificate renewal
|
||||||
|
- [X] Restrict to safe ciphersuites and TLS versions
|
||||||
|
- [ ] TLS stack configuration loaded from `step-ca`
|
||||||
|
- [ ] Root certificate rotation
|
||||||
|
- [X] Client using autocert root certificate
|
||||||
|
- [X] mTLS (send client certificate if server asks for it)
|
||||||
|
- [ ] Automatic certificate rotation
|
||||||
|
- [X] Restrict to safe ciphersuites and TLS versions
|
||||||
|
- [ ] TLS stack configuration loaded from `step-ca`
|
||||||
|
- [ ] Root certificate rotation
|
||||||
|
|
||||||
|
[curl/](curl/)
|
||||||
|
- [X] Client
|
||||||
|
- [X] mTLS (send client certificate if server asks for it)
|
||||||
|
- [X] Automatic certificate rotation
|
||||||
|
- [ ] Restrict to safe ciphersuites and TLS versions
|
||||||
|
- [ ] TLS stack configuration loaded from `step-ca`
|
||||||
|
- [ ] Root certificate rotation
|
||||||
|
|
5
autocert/examples/hello-mtls/curl/Dockerfile.client
Normal file
5
autocert/examples/hello-mtls/curl/Dockerfile.client
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
FROM alpine
|
||||||
|
RUN apk add --no-cache bash curl
|
||||||
|
COPY client.sh .
|
||||||
|
RUN chmod +x client.sh
|
||||||
|
ENTRYPOINT ./client.sh
|
11
autocert/examples/hello-mtls/curl/client.sh
Normal file
11
autocert/examples/hello-mtls/curl/client.sh
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
#!/bin/bash
|
||||||
|
while :
|
||||||
|
do
|
||||||
|
response=$(curl -sS \
|
||||||
|
--cacert /var/run/autocert.step.sm/root.crt \
|
||||||
|
--cert /var/run/autocert.step.sm/site.crt \
|
||||||
|
--key /var/run/autocert.step.sm/site.key \
|
||||||
|
${HELLO_MTLS_URL})
|
||||||
|
echo "$(date): ${response}"
|
||||||
|
sleep 5
|
||||||
|
done
|
22
autocert/examples/hello-mtls/curl/hello-mtls.client.yaml
Normal file
22
autocert/examples/hello-mtls/curl/hello-mtls.client.yaml
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: hello-mtls-client
|
||||||
|
labels: {app: hello-mtls-client}
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector: {matchLabels: {app: hello-mtls-client}}
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
autocert.step.sm/name: hello-mtls-client.default.pod.cluster.local
|
||||||
|
labels: {app: hello-mtls-client}
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: hello-mtls-client
|
||||||
|
image: hello-mtls-client-curl:latest
|
||||||
|
imagePullPolicy: Never
|
||||||
|
resources: {requests: {cpu: 10m, memory: 20Mi}}
|
||||||
|
env:
|
||||||
|
- name: HELLO_MTLS_URL
|
||||||
|
value: https://hello-mtls.default.svc.cluster.local
|
10
autocert/examples/hello-mtls/go/Dockerfile.client
Normal file
10
autocert/examples/hello-mtls/go/Dockerfile.client
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
# build stage
|
||||||
|
FROM golang:alpine AS build-env
|
||||||
|
RUN mkdir /src
|
||||||
|
ADD client.go /src
|
||||||
|
RUN cd /src && go build -o client
|
||||||
|
|
||||||
|
# final stage
|
||||||
|
FROM alpine
|
||||||
|
COPY --from=build-env /src/client .
|
||||||
|
ENTRYPOINT ./client
|
10
autocert/examples/hello-mtls/go/Dockerfile.server
Normal file
10
autocert/examples/hello-mtls/go/Dockerfile.server
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
# build stage
|
||||||
|
FROM golang:alpine AS build-env
|
||||||
|
RUN mkdir /src
|
||||||
|
ADD server.go /src
|
||||||
|
RUN cd /src && go build -o server
|
||||||
|
|
||||||
|
# final stage
|
||||||
|
FROM alpine
|
||||||
|
COPY --from=build-env /src/server .
|
||||||
|
ENTRYPOINT ./server
|
85
autocert/examples/hello-mtls/go/client.go
Normal file
85
autocert/examples/hello-mtls/go/client.go
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
autocertFile = "/var/run/autocert.step.sm/site.crt"
|
||||||
|
autocertKey = "/var/run/autocert.step.sm/site.key"
|
||||||
|
autocertRoot = "/var/run/autocert.step.sm/root.crt"
|
||||||
|
requestFrequency = 5 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
func loadRootCertPool() (*x509.CertPool, error) {
|
||||||
|
root, err := ioutil.ReadFile(autocertRoot)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pool := x509.NewCertPool()
|
||||||
|
if ok := pool.AppendCertsFromPEM(root); !ok {
|
||||||
|
return nil, errors.New("Missing or invalid root certificate")
|
||||||
|
}
|
||||||
|
|
||||||
|
return pool, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
url := os.Getenv("HELLO_MTLS_URL")
|
||||||
|
|
||||||
|
// Read our leaf certificate and key from disk
|
||||||
|
cert, err := tls.LoadX509KeyPair(autocertFile, autocertKey)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the root certificate for our CA from disk
|
||||||
|
roots, err := loadRootCertPool()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create an HTTPS client using our cert, key & pool
|
||||||
|
client := &http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{
|
||||||
|
RootCAs: roots,
|
||||||
|
Certificates: []tls.Certificate{cert},
|
||||||
|
MinVersion: tls.VersionTLS12,
|
||||||
|
CurvePreferences: []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256},
|
||||||
|
CipherSuites: []uint16{
|
||||||
|
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
|
||||||
|
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
// Make request
|
||||||
|
r, err := client.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer r.Body.Close()
|
||||||
|
body, err := ioutil.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("%s: %s\n", time.Now().Format(time.RFC3339), strings.Trim(string(body), "\n"))
|
||||||
|
|
||||||
|
time.Sleep(requestFrequency)
|
||||||
|
}
|
||||||
|
}
|
22
autocert/examples/hello-mtls/go/hello-mtls.client.yaml
Normal file
22
autocert/examples/hello-mtls/go/hello-mtls.client.yaml
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: hello-mtls-client
|
||||||
|
labels: {app: hello-mtls-client}
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector: {matchLabels: {app: hello-mtls-client}}
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
autocert.step.sm/name: hello-mtls-client.default.pod.cluster.local
|
||||||
|
labels: {app: hello-mtls-client}
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: hello-mtls-client
|
||||||
|
image: hello-mtls-client-go:latest
|
||||||
|
imagePullPolicy: Never
|
||||||
|
resources: {requests: {cpu: 10m, memory: 20Mi}}
|
||||||
|
env:
|
||||||
|
- name: HELLO_MTLS_URL
|
||||||
|
value: https://hello-mtls.default.svc.cluster.local
|
33
autocert/examples/hello-mtls/go/hello-mtls.server.yaml
Normal file
33
autocert/examples/hello-mtls/go/hello-mtls.server.yaml
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
labels: {app: hello-mtls}
|
||||||
|
name: hello-mtls
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
ports:
|
||||||
|
- port: 443
|
||||||
|
targetPort: 443
|
||||||
|
selector: {app: hello-mtls}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: hello-mtls
|
||||||
|
labels: {app: hello-mtls}
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector: {matchLabels: {app: hello-mtls}}
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
autocert.step.sm/name: hello-mtls.default.svc.cluster.local
|
||||||
|
labels: {app: hello-mtls}
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: hello-mtls
|
||||||
|
image: hello-mtls-server-go:latest
|
||||||
|
imagePullPolicy: Never
|
||||||
|
resources: {requests: {cpu: 10m, memory: 20Mi}}
|
136
autocert/examples/hello-mtls/go/server.go
Normal file
136
autocert/examples/hello-mtls/go/server.go
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
autocertFile = "/var/run/autocert.step.sm/site.crt"
|
||||||
|
autocertKey = "/var/run/autocert.step.sm/site.key"
|
||||||
|
autocertRoot = "/var/run/autocert.step.sm/root.crt"
|
||||||
|
tickFrequency = 15 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
// Uses techniques from https://diogomonica.com/2017/01/11/hitless-tls-certificate-rotation-in-go/
|
||||||
|
// to automatically rotate certificates when they're renewed.
|
||||||
|
|
||||||
|
type rotator struct {
|
||||||
|
sync.Mutex
|
||||||
|
certificate *tls.Certificate
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *rotator) getCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||||
|
r.Lock()
|
||||||
|
defer r.Unlock()
|
||||||
|
|
||||||
|
return r.certificate, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *rotator) loadCertificate(certFile, keyFile string) error {
|
||||||
|
r.Lock()
|
||||||
|
defer r.Unlock()
|
||||||
|
|
||||||
|
c, err := tls.LoadX509KeyPair(certFile, keyFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
r.certificate = &c
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadRootCertPool() (*x509.CertPool, error) {
|
||||||
|
root, err := ioutil.ReadFile(autocertRoot)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pool := x509.NewCertPool()
|
||||||
|
if ok := pool.AppendCertsFromPEM(root); !ok {
|
||||||
|
return nil, errors.New("Missing or invalid root certificate")
|
||||||
|
}
|
||||||
|
|
||||||
|
return pool, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.TLS == nil || len(r.TLS.PeerCertificates) == 0 {
|
||||||
|
fmt.Fprintf(w, "Unauthenticated")
|
||||||
|
} else {
|
||||||
|
name := r.TLS.PeerCertificates[0].Subject.CommonName
|
||||||
|
fmt.Fprintf(w, "Hello, %s!\n", name)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
fmt.Fprintf(w, "Ok\n")
|
||||||
|
})
|
||||||
|
|
||||||
|
roots, err := loadRootCertPool()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r := &rotator{}
|
||||||
|
cfg := &tls.Config{
|
||||||
|
ClientAuth: tls.RequireAndVerifyClientCert,
|
||||||
|
ClientCAs: roots,
|
||||||
|
MinVersion: tls.VersionTLS12,
|
||||||
|
CurvePreferences: []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256},
|
||||||
|
PreferServerCipherSuites: true,
|
||||||
|
CipherSuites: []uint16{
|
||||||
|
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
|
||||||
|
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||||
|
},
|
||||||
|
GetCertificate: r.getCertificate,
|
||||||
|
}
|
||||||
|
srv := &http.Server{
|
||||||
|
Addr: ":443",
|
||||||
|
Handler: mux,
|
||||||
|
TLSConfig: cfg,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load certificate
|
||||||
|
err = r.loadCertificate(autocertFile, autocertKey)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Error loading certificate and key", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schedule periodic re-load of certificate
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(tickFrequency)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
fmt.Println("Checking for new certificate...")
|
||||||
|
err := r.loadCertificate(autocertFile, autocertKey)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Error loading certificate and key", err)
|
||||||
|
}
|
||||||
|
case <- done:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
defer close(done)
|
||||||
|
|
||||||
|
log.Println("Listening no :443")
|
||||||
|
|
||||||
|
// Start serving HTTPS
|
||||||
|
err = srv.ListenAndServeTLS("", "")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("ListenAndServerTLS: ", err)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue