Skip to content

Commit

Permalink
Finish TLS for API frontend
Browse files Browse the repository at this point in the history
  • Loading branch information
immesys committed May 26, 2018
1 parent ce58ca9 commit cec53dd
Show file tree
Hide file tree
Showing 7 changed files with 191 additions and 13 deletions.
2 changes: 1 addition & 1 deletion Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions containers/apifrontend/rebuild.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#!/bin/bash
set -ex

pushd ../../tools/apifrontend
go build -v
ver=$(./apifrontend -version)
popd
cp ../../tools/apifrontend/apifrontend .
docker build -t btrdb/dev-apifrontend:${ver} .
docker push btrdb/dev-apifrontend:${ver}
docker tag btrdb/dev-apifrontend:${ver} btrdb/dev-apifrontend:latest
docker push btrdb/dev-apifrontend:latest
14 changes: 7 additions & 7 deletions manifest_templates/apifrontend.deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,18 @@ spec:
- name: ETCD_ENDPOINT
value: http://etcd:2379
- name: DISABLE_INSECURE
value: NO
value: "NO"
- name: BTRDB_ENDPOINTS
value: btrdb-bootstrap:4410{{if gt (len .SiteInfo.ExternalIPs) 0 }}
- name: EXTERNAL_ADDRESS
value: {{ index .SiteInfo.ExternalIPs 0 }}:4410{{end}}
ports:
- containerPort: 4410
protocol: TCP
name: grpc_insecure
name: grpc-insecure
- containerPort: 4411
protocol: TCP
name: grpc_secure
name: grpc-secure
- containerPort: 9000
protocol: TCP
name: http
Expand All @@ -48,11 +48,11 @@ metadata:
spec:
ports:
- port: 4410
targetPort: grpc_insecure
name: grpc_insecure
targetPort: grpc-insecure
name: grpc-insecure
- port: 4411
targetPort: grpc_secure
name: grpc_secure
targetPort: grpc-secure
name: grpc-secure
- port: 9000
targetPort: http
name: http
Expand Down
2 changes: 1 addition & 1 deletion mfgen/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (

//go:generate go-bindata -o templates.go -prefix ../manifest_templates ../manifest_templates/

const PackageVersion = "4.10.1"
const PackageVersion = "4.10.2"

func main() {
app := cli.NewApp()
Expand Down
4 changes: 2 additions & 2 deletions tools/apifrontend/cli/frontendcli.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,12 @@ func NewFrontendModule(c *etcd.Client) admincli.CLIModule {
}

func setkey(c *etcd.Client, keysuffix string, value string) error {
_, err := etcdConn.Put(context.Background(), "api/"+keysuffix, value)
_, err := c.Put(context.Background(), "api/"+keysuffix, value)
return err
}

func getkey(c *etcd.Client, keysuffix string) (string, error) {
resp, err := etcdConn.Get(context.Background(), "api/"+keysuffix)
resp, err := c.Get(context.Background(), "api/"+keysuffix)
if err != nil {
return "", err
}
Expand Down
12 changes: 10 additions & 2 deletions tools/apifrontend/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,11 +124,16 @@ func ProxyGRPCSecure(laddr string) *tls.Config {
}
switch mode {
case "autocert":
cfg, err = cli.GetAPIFrontendAutocertTLS(etcdClient)
cfg, err = MRPlottersAutocertTLSConfig(etcdClient)
if err != nil {
fmt.Printf("could not set up autocert: %v\n", err)
os.Exit(1)
}
if cfg == nil {
fmt.Printf("mrplotter autocert not set up fully\n")
os.Exit(1)
}
fmt.Printf("successfully loaded mrplotter's cert\n")
case "hardcoded":
cert, key, err := cli.GetAPIFrontendHardcoded(etcdClient)
if err != nil {
Expand All @@ -147,7 +152,7 @@ func ProxyGRPCSecure(laddr string) *tls.Config {
},
}
default:
fmt.Printf("WARNING! secure API disabled in admin console\n")
fmt.Printf("WARNING! secure API disabled in api frontend\n")
return nil
}

Expand Down Expand Up @@ -237,13 +242,16 @@ func main() {
}()
}
if tlsconfig != nil {
fmt.Printf("starting secure http\n")
go func() {
server := &http.Server{Addr: ":9001", Handler: mux, TLSConfig: tlsconfig}
err := server.ListenAndServeTLS("", "")
if err != nil {
panic(err)
}
}()
} else {
fmt.Printf("skipping secure http\n")
}
for {
time.Sleep(10 * time.Second)
Expand Down
158 changes: 158 additions & 0 deletions tools/apifrontend/tls.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
package main

import (
"context"
"crypto"
"crypto/ecdsa"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"errors"
"fmt"
"math/big"
"strings"
"time"

etcd "github.com/coreos/etcd/clientv3"
)

// This was taken from https://golang.org/src/crypto/tls/generate_cert.go.
Expand Down Expand Up @@ -88,3 +95,154 @@ func SelfSignedCertificate(dnsNames []string) (*pem.Block, *pem.Block, error) {
}
return cert, key, nil
}

func MRPlottersAutocertTLSConfig(c *etcd.Client) (*tls.Config, error) {
hn, err := c.Get(context.Background(), "mrplotter/keys/hostname")
if err != nil {
return nil, err
}
if len(hn.Kvs) == 0 {
return nil, nil
}
hostname := string(hn.Kvs[0].Value)
ci, err := c.Get(context.Background(), "mrplotter/keys/autocert_cache/"+hostname)
if err != nil {
return nil, err
}
if len(ci.Kvs) == 0 {
return nil, nil
}
cert, err := certificateFromAutocertCache(hostname, ci.Kvs[0].Value)
if err != nil {
return nil, err
}
return &tls.Config{
GetCertificate: func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
return cert, nil
},
}, nil
}

// cacheGet always returns a valid certificate, or an error otherwise.
// If a cached certficate exists but is not valid, ErrCacheMiss is returned.
func certificateFromAutocertCache(domain string, data []byte) (*tls.Certificate, error) {

// private
priv, pub := pem.Decode(data)
if priv == nil || !strings.Contains(priv.Type, "PRIVATE") {
return nil, fmt.Errorf("certificate is corrupt")
}
privKey, err := parsePrivateKey(priv.Bytes)
if err != nil {
return nil, err
}

// public
var pubDER [][]byte
for len(pub) > 0 {
var b *pem.Block
b, pub = pem.Decode(pub)
if b == nil {
break
}
pubDER = append(pubDER, b.Bytes)
}
if len(pub) > 0 {
// Leftover content not consumed by pem.Decode. Corrupt. Ignore.
return nil, fmt.Errorf("certificate is corrupt")
}

// verify and create TLS cert
leaf, err := validCert(domain, pubDER, privKey)
if err != nil {
return nil, fmt.Errorf("certificate is invalid")
}
tlscert := &tls.Certificate{
Certificate: pubDER,
PrivateKey: privKey,
Leaf: leaf,
}
return tlscert, nil
}

// validCert parses a cert chain provided as der argument and verifies the leaf, der[0],
// corresponds to the private key, as well as the domain match and expiration dates.
// It doesn't do any revocation checking.
//
// The returned value is the verified leaf cert.
func validCert(domain string, der [][]byte, key crypto.Signer) (leaf *x509.Certificate, err error) {
// parse public part(s)
var n int
for _, b := range der {
n += len(b)
}
pub := make([]byte, n)
n = 0
for _, b := range der {
n += copy(pub[n:], b)
}
x509Cert, err := x509.ParseCertificates(pub)
if len(x509Cert) == 0 {
return nil, errors.New("acme/autocert: no public key found")
}
// verify the leaf is not expired and matches the domain name
leaf = x509Cert[0]
now := time.Now()
if now.Before(leaf.NotBefore) {
return nil, errors.New("acme/autocert: certificate is not valid yet")
}
if now.After(leaf.NotAfter) {
return nil, errors.New("acme/autocert: expired certificate")
}
if err := leaf.VerifyHostname(domain); err != nil {
return nil, err
}
// ensure the leaf corresponds to the private key
switch pub := leaf.PublicKey.(type) {
case *rsa.PublicKey:
prv, ok := key.(*rsa.PrivateKey)
if !ok {
return nil, errors.New("acme/autocert: private key type does not match public key type")
}
if pub.N.Cmp(prv.N) != 0 {
return nil, errors.New("acme/autocert: private key does not match public key")
}
case *ecdsa.PublicKey:
prv, ok := key.(*ecdsa.PrivateKey)
if !ok {
return nil, errors.New("acme/autocert: private key type does not match public key type")
}
if pub.X.Cmp(prv.X) != 0 || pub.Y.Cmp(prv.Y) != 0 {
return nil, errors.New("acme/autocert: private key does not match public key")
}
default:
return nil, errors.New("acme/autocert: unknown public key algorithm")
}
return leaf, nil
}

// Attempt to parse the given private key DER block. OpenSSL 0.9.8 generates
// PKCS#1 private keys by default, while OpenSSL 1.0.0 generates PKCS#8 keys.
// OpenSSL ecparam generates SEC1 EC private keys for ECDSA. We try all three.
//
// Inspired by parsePrivateKey in crypto/tls/tls.go.
func parsePrivateKey(der []byte) (crypto.Signer, error) {
if key, err := x509.ParsePKCS1PrivateKey(der); err == nil {
return key, nil
}
if key, err := x509.ParsePKCS8PrivateKey(der); err == nil {
switch key := key.(type) {
case *rsa.PrivateKey:
return key, nil
case *ecdsa.PrivateKey:
return key, nil
default:
return nil, errors.New("acme/autocert: unknown private key type in PKCS#8 wrapping")
}
}
if key, err := x509.ParseECPrivateKey(der); err == nil {
return key, nil
}

return nil, errors.New("acme/autocert: failed to parse private key")
}

0 comments on commit cec53dd

Please sign in to comment.