Skip to content

Instantly share code, notes, and snippets.

@wranders
Last active February 16, 2020 00:23
Show Gist options
  • Save wranders/a8d6a5fc7eba2310d0cabf7b9d545d59 to your computer and use it in GitHub Desktop.
Save wranders/a8d6a5fc7eba2310d0cabf7b9d545d59 to your computer and use it in GitHub Desktop.
Go HTTPS Server with Nginx Client Certificate Authenication/Encryption

Go TLS Server with Nginx Client Certificate Authenication/Encryption

This Gist demonstrates the use of a Go TLS server behind an Nginx reverse proxy and backend communication over TLS with Client Authentication certificates.

This is useful when an end server is not on the same host as Nginx and communication must still be encrypted. Since Nginx cannot pass-through TLS sessions, it must terminate the Client session and pass information over its own.

For trust to be established, a common Root Certificate Authority (CA) must be used. gencert.go is run to generate the Root CA certificate, and sign serverAuth certificates for the Go TLS server and Nginx frontend, as well as the clientAuth certificate that allows Nginx to authenticate and communicate with the Go TLS server. The certificate and key used on the Nginx frontend do not have to be issued from the generated Root CA and can be from any other publicly trusted source (ie. LetsEncrypt), but the client certificate used must be issued by the root CA configured on the Go TLS server.

The Go TLS server is configured to only accept connections that present a clientAuth certificate from the Root CA and will render an error if accessed directly, preventing communication with any party not in possession of a trusted clientAuth certificate.

Furthermore, Nginx is configured to pass the Forwarded header to the Go TLS server that contains a shared secret. The Go TLS server uses the proxySecretMiddleware to determine that the message originates from the desired source and renders a 401 - Unauthorized error if it doesn't. This prevents communication from other parties in possession of a valid clientAuth certificate should those certificates be issued for other purposes.

Additional Nginx locations have been configured to demonstrate the responses served in the event of an incorrect (/bad) or missing (/none) Forwarded header.


gencert.go
  Generates a Root CA and required public/private key pairs.
  Files are created in executing directory (PWD).
  
  Usage:
    -backip   [STRING] (IP Address of the backend (Go) server, used in x509 SAN) (Default: "127.0.0.1")
    -backdn   [STRING] (Domain Name of the backend (Go) server, used in x509 CN and SAN) (Default: "localhost")
    -frontip  [STRING] (IP Address of the frontend (Nginx) server, used in x509 SAN) (Default: "127.0.0.1")
    -frontdn  [STRING] (Domain Name of the frontend (Nginx) server, used in x509 CN and SAN) (Default: "localhost")
server.go
  Starts a TLS server with restricted Client Authentication.
  
  Usage:
    -listen [STRING] (Listen address and port of the TLS server) (Default: ":8443")
    -cacert [STRING] (Root CA Public Certificate location) (Default: "./ca.crt")
    -cert   [STRING] (TLS Server Public Certificate location) (Default: "./backend.crt")
    -key    [STRING] (TLS Server Private Key location) (Default: "./backend.key")
package main
import (
"bytes"
"crypto/rand"
"crypto/rsa"
"crypto/sha1"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"flag"
"fmt"
"io/ioutil"
"log"
"math/big"
"net"
"time"
)
// CertificateKeyPair contains the public x509 certificate and
// the private RSA key. Signed certificates are stored as Bytes
type CertificateKeyPair struct {
Cert *x509.Certificate
CertBytes []byte
Key *rsa.PrivateKey
}
func main() {
backendIP := flag.String(
"backip",
"127.0.0.1",
"Backend server address (Go Server)",
)
backendDN := flag.String(
"backdn",
"localhost",
"Backend server domain name (Go Server",
)
frontendIP := flag.String(
"frontip",
"127.0.0.1",
"Frontend server address (proxy)",
)
frontendDN := flag.String(
"frontdn",
"localhost",
"Frontend server domain name (proxy)",
)
flag.Parse()
// Create a new Certificate Authority
ca, err := NewCertificateAuthority("Local CA")
if err != nil {
log.Fatalln(err)
}
if err := ca.WritePublicCertificate("./ca.crt"); err != nil {
log.Fatalln(err)
}
// Create a new public certificate and private key for
// the backend server
backend, err := NewServerCertificateKeyPair(*backendIP, *backendDN, ca)
if err != nil {
log.Fatalln(err)
}
if err := backend.WritePublicCertificate("./backend.crt"); err != nil {
log.Fatalln(err)
}
if err := backend.WritePrivateKey("./backend.key"); err != nil {
log.Fatalln(err)
}
// Create a new public certificate and private key for
// the frontend proxy
frontend, err := NewServerCertificateKeyPair(*frontendIP, *frontendDN, ca)
if err != nil {
log.Fatalln(err)
}
if err := frontend.WritePublicCertificate("./frontend.crt"); err != nil {
log.Fatalln(err)
}
if err := frontend.WritePrivateKey("./frontend.key"); err != nil {
log.Fatalln(err)
}
// Create a new public certificate and private key for
// the proxy to authenticate to the backend server
frontendClient, err := NewClientCertificateKeyPair("Frontend Client", ca)
if err != nil {
log.Fatalln(err)
}
if err := frontendClient.WritePublicCertificate("./client.crt"); err != nil {
log.Fatalln(err)
}
if err := frontendClient.WritePrivateKey("./client.key"); err != nil {
log.Fatalln(err)
}
}
// WritePublicCertificate writes a PEM-encoded public
// certificate to the specified file
func (c *CertificateKeyPair) WritePublicCertificate(file string) error {
return c.writeFile(file, &pem.Block{
Type: "CERTIFICATE",
Bytes: c.CertBytes,
})
}
// WritePrivateKey writes a PEM-encoded private key to the
// specified file
func (c *CertificateKeyPair) WritePrivateKey(file string) error {
return c.writeFile(file, &pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(c.Key),
})
}
func (*CertificateKeyPair) writeFile(file string, block *pem.Block) error {
enc := new(bytes.Buffer)
pem.Encode(enc, block)
err := ioutil.WriteFile(file, enc.Bytes(), 0777)
if err != nil {
return err
}
return nil
}
// NewCertificateAuthority generates a new certificate authority
// for signing server and client certificates
func NewCertificateAuthority(name string) (*CertificateKeyPair, error) {
log.Println("Generating new CA certificate and key pair...")
serial, err := newSerial()
if err != nil {
return nil, fmt.Errorf("Error generating CA serial: %w", err)
}
key, err := rsa.GenerateKey(rand.Reader, 4096)
if err != nil {
return nil, fmt.Errorf("Error generating CA private key: %w", err)
}
cert := &x509.Certificate{
SerialNumber: serial,
Subject: pkix.Name{
CommonName: name,
},
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(10, 0, 0),
SubjectKeyId: hashKeyID(key.N),
KeyUsage: x509.KeyUsageCertSign,
BasicConstraintsValid: true,
IsCA: true,
MaxPathLenZero: true,
}
ca := &CertificateKeyPair{
Cert: cert,
Key: key,
}
ca, err = SignCertificate(ca, ca)
if err != nil {
return ca, fmt.Errorf("Error signing CA certificate: %w", err)
}
return ca, nil
}
// NewServerCertificateKeyPair generates a new certificate key
// pair for use with web servers
func NewServerCertificateKeyPair(ip string, dn string, ca *CertificateKeyPair) (*CertificateKeyPair, error) {
log.Printf("Generating new server certificate key pair for %s(%s)\n", dn, ip)
serial, err := newSerial()
if err != nil {
return nil, fmt.Errorf("Error generating certificate serial: %w", err)
}
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, fmt.Errorf("Error generating private key: %w", err)
}
cert := &x509.Certificate{
SerialNumber: serial,
Subject: pkix.Name{
CommonName: dn,
},
IPAddresses: []net.IP{
net.ParseIP(ip),
},
DNSNames: []string{
dn,
},
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(1, 0, 0),
SubjectKeyId: hashKeyID(key.N),
AuthorityKeyId: hashKeyID(ca.Key.N),
ExtKeyUsage: []x509.ExtKeyUsage{
x509.ExtKeyUsageServerAuth,
},
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
}
pair := &CertificateKeyPair{
Cert: cert,
Key: key,
}
pair, err = SignCertificate(pair, ca)
if err != nil {
return pair, fmt.Errorf("Error signing certificate: %w", err)
}
return pair, nil
}
// NewClientCertificateKeyPair generates a client certificate for
// authenticating and encrypting communications to a server
func NewClientCertificateKeyPair(name string, ca *CertificateKeyPair) (*CertificateKeyPair, error) {
log.Printf("Generating new client certificate key pair for %s\n", name)
serial, err := newSerial()
if err != nil {
return nil, fmt.Errorf("Error generating certificate serial: %w", err)
}
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, fmt.Errorf("Error generating private key: %w", err)
}
cert := &x509.Certificate{
SerialNumber: serial,
Subject: pkix.Name{
CommonName: name,
},
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(1, 0, 0),
SubjectKeyId: hashKeyID(key.N),
AuthorityKeyId: hashKeyID(ca.Key.N),
ExtKeyUsage: []x509.ExtKeyUsage{
x509.ExtKeyUsageClientAuth,
},
KeyUsage: x509.KeyUsageDigitalSignature,
}
pair := &CertificateKeyPair{
Cert: cert,
Key: key,
}
pair, err = SignCertificate(pair, ca)
if err != nil {
return pair, fmt.Errorf("Error signing certificate: %w", err)
}
return pair, nil
}
// SignCertificate signs the provided certificate with the
// provided certificate authority
func SignCertificate(cert *CertificateKeyPair, ca *CertificateKeyPair) (*CertificateKeyPair, error) {
bytes, err := x509.CreateCertificate(
rand.Reader,
cert.Cert,
ca.Cert,
&cert.Key.PublicKey,
ca.Key,
)
if err != nil {
return cert, fmt.Errorf("Error signing certificate: %w", err)
}
cert.CertBytes = bytes
return cert, nil
}
func newSerial() (*big.Int, error) {
max := new(big.Int)
max.Exp(
big.NewInt(2),
big.NewInt(128),
nil,
)
max.Sub(
max,
big.NewInt(1),
)
n, err := rand.Int(rand.Reader, max)
if err != nil {
return n, err
}
return n, nil
}
func hashKeyID(n *big.Int) []byte {
h := sha1.New()
h.Write(n.Bytes())
return h.Sum(nil)
}
server {
listen 443 ssl http2;
server_name _;
ssl_certificate ./frontend.crt;
ssl_certificate_key ./frontend.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
proxy_ssl_certificate ./client.crt;
proxy_ssl_certificate_key ./client.key;
proxy_ssl_trusted_certificate ./ca.crt;
proxy_ssl_verify on;
proxy_ssl_verify_depth 1;
proxy_ssl_session_reuse on;
location / {
proxy_pass https://localhost:8443;
proxy_set_header Forwarded "secret=mysupersecret";
}
location /bad {
proxy_pass https://localhost:8443;
proxy_set_header Forwarded "secret=whoops";
}
location /none {
proxy_pass https://localhost:8443;
}
}
package main
import (
"crypto/tls"
"crypto/x509"
"flag"
"io/ioutil"
"log"
"net/http"
"os"
"strings"
)
var proxySecret = "mysupersecret"
func main() {
listen := flag.String("listen", ":8443", "Host and port to listen to")
caCert := flag.String("cacert", "./ca.crt", "Root certificate")
srvCert := flag.String("cert", "./backend.crt", "Server certificate")
srvKey := flag.String("key", "./backend.key", "Server key")
flag.Parse()
if !fileExists(*caCert) {
log.Fatalln("CA Certificate cannot be found")
}
if !fileExists(*srvCert) {
log.Fatalln("Server Certificate cannot be found")
}
if !fileExists(*srvKey) {
log.Fatalln("Server Private Key cannot be found")
}
cacertPEM, err := ioutil.ReadFile(*caCert)
if err != nil {
log.Fatalln(err)
}
roots := x509.NewCertPool()
ok := roots.AppendCertsFromPEM(cacertPEM)
if !ok {
log.Fatalln("Failed to parse CA certificate")
}
srvPair, err := tls.LoadX509KeyPair(*srvCert, *srvKey)
if err != nil {
log.Fatalln(err)
}
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{srvPair},
ClientAuth: tls.RequireAndVerifyClientCert,
ClientCAs: roots,
MinVersion: tls.VersionTLS12,
CurvePreferences: []tls.CurveID{
tls.CurveP521, tls.CurveP384, tls.CurveP256,
},
PreferServerCipherSuites: true,
CipherSuites: []uint16{
tls.TLS_AES_128_GCM_SHA256,
tls.TLS_AES_256_GCM_SHA384,
tls.TLS_CHACHA20_POLY1305_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
},
}
srvMux := http.NewServeMux()
srvMux.Handle("/", proxySecretMiddleware(helloHandler()))
srv := &http.Server{
Addr: *listen,
Handler: srvMux,
TLSConfig: tlsConfig,
}
log.Printf("Listening on %s", *listen)
log.Fatal(srv.ListenAndServeTLS(*srvCert, *srvKey))
}
func fileExists(file string) bool {
info, err := os.Stat(file)
if os.IsNotExist(err) {
return false
}
return !info.IsDir()
}
func helloHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Strict-Transport-Security", "max-age=63072000")
w.Write([]byte("Hello from the other side!\n"))
})
}
func proxySecretMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fm := splitForwardedHeader(r.Header.Get("Forwarded"))
if fm["secret"] != proxySecret {
w.WriteHeader(http.StatusUnauthorized)
log.Println("Unauthorized Upstream")
} else {
next.ServeHTTP(w, r)
}
})
}
func splitForwardedHeader(header string) map[string]string {
m := make(map[string]string)
for _, a := range strings.Split(header, ";") {
b := strings.Split(a, "=")
m[b[0]] = b[1]
}
return m
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment