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