Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tls: generate server TLS cert from CA #2401

Merged
merged 4 commits into from
Jul 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ test/update:
go test ./internal/cmd -test.update-golden

dev:
docker build . -t infrahq/infra:dev
docker buildx build . -t infrahq/infra:dev
kubectl config use-context docker-desktop
helm upgrade --install --wait \
--set global.image.pullPolicy=Never \
Expand Down
6 changes: 6 additions & 0 deletions helm/charts/infra/templates/server/configmap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ data:
{{- end }}
{{- end }}

{{- if not .Values.server.config.tls }}
tls:
ca: "/var/run/secrets/infrahq.com/tls-ca/ca.crt"
caPrivateKey: "file:/var/run/secrets/infrahq.com/tls-ca/ca.key"
{{- end }}

providers:
{{- .Values.server.additionalProviders | default list | concat .Values.server.config.providers | uniq | toYaml | nindent 6 }}

Expand Down
9 changes: 9 additions & 0 deletions helm/charts/infra/templates/server/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ spec:
- name: conf
mountPath: /etc/infrahq
readOnly: true
{{- if (not .Values.server.config.tls) }}
- name: tls-ca
mountPath: /var/run/secrets/infrahq.com/tls-ca
{{- end }}
{{- if .Values.server.persistence.enabled }}
- name: data
mountPath: /var/lib/infrahq/server
Expand Down Expand Up @@ -89,6 +93,11 @@ spec:
- name: conf
configMap:
name: {{ include "server.fullname" . }}
{{- if (not .Values.server.config.tls) }}
- name: tls-ca
BruceMacD marked this conversation as resolved.
Show resolved Hide resolved
secret:
secretName: {{ include "server.fullname" . }}-ca
{{- end }}
{{- if .Values.server.persistence.enabled }}
- name: data
persistentVolumeClaim:
Expand Down
29 changes: 29 additions & 0 deletions helm/charts/infra/templates/server/secret.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@

{{- if include "server.enabled" . | eq "true" }}
{{- if (not .Values.server.config.tls) }}

apiVersion: v1
kind: Secret
metadata:
name: {{ include "server.fullname" . }}-ca
labels:
{{- include "server.labels" . | nindent 4 }}
data:

{{- $secret := lookup "v1" "Secret" .Release.Namespace (printf "%s-ca" (include "server.fullname" .)) -}}
{{- if $secret.data }}
ca.crt: |
{{- get $secret.data "ca.crt" | nindent 4 }}
ca.key: |
BruceMacD marked this conversation as resolved.
Show resolved Hide resolved
{{- get $secret.data "ca.key" | nindent 4 }}

{{- else }}
{{- $ca := genCA "Infra Server" 3650 }}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the name people will see in the Subject line of the certificate. Should we use "Infra" , or "Infra Server" ?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Infra Server" seems fine to me 👍

ca.crt: |
{{- $ca.Cert | b64enc | nindent 4 }}
ca.key: |
{{- $ca.Key | b64enc | nindent 4 }}

{{- end }}{{/* if secret.data */}}
{{- end }}{{/* if not tls */}}
{{- end }}{{/* if server.enabled */}}
13 changes: 13 additions & 0 deletions helm/charts/infra/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,19 @@ server:
# - name: [email protected]
# password: file:/var/run/secrets/[email protected]

# TLS configuration for the API server. Defaults to generating a self-signed CA and
# generating certificates from that CA.
tls: {}
mxyng marked this conversation as resolved.
Show resolved Hide resolved
mxyng marked this conversation as resolved.
Show resolved Hide resolved

# Configure a CA and private key using files
# ca: /path/to/ca.crt
# caPrivateKey: file:/path/to/ca.key

# Configure a TLS certificate and private key using files
# certificate: /path/to/server.crt
# privateKey: file:/path/to/server.key


## Default connector configurations
connector:

Expand Down
108 changes: 0 additions & 108 deletions internal/certs/certs.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
Expand All @@ -14,10 +13,6 @@ import (
"net"
"strings"
"time"

"golang.org/x/crypto/acme/autocert"

"github.com/infrahq/infra/internal/logging"
)

func GenerateCertificate(hosts []string, caCert *x509.Certificate, caKey crypto.PrivateKey) (certPEM []byte, keyPEM []byte, err error) {
Expand Down Expand Up @@ -62,68 +57,6 @@ func GenerateCertificate(hosts []string, caCert *x509.Certificate, caKey crypto.
return PEMEncodeCertificate(certBytes), keyBytes, nil
}

func SelfSignedOrLetsEncryptCert(manager *autocert.Manager) func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved this into internal/server/tls.go and renamed it to getCertificate. That's the only place it is used.

return func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
ctx := hello.Context()
cert, err := manager.GetCertificate(hello)
if err == nil {
return cert, nil
}

serverName := hello.ServerName

if serverName == "" {
serverName, _, err = net.SplitHostPort(hello.Conn.LocalAddr().String())
if err != nil {
return nil, err
}
}

certBytes, err := manager.Cache.Get(ctx, serverName+".crt")
if err != nil {
logging.Warnf("cert: %s", err)
}

keyBytes, err := manager.Cache.Get(ctx, serverName+".key")
if err != nil {
logging.Warnf("key: %s", err)
}

// if either cert or key is missing, create it
if certBytes == nil || keyBytes == nil {
ca, caPrivKey, err := newCA()
if err != nil {
return nil, err
}

certBytes, keyBytes, err = GenerateCertificate([]string{serverName}, ca, caPrivKey)
if err != nil {
return nil, err
}

if err := manager.Cache.Put(ctx, serverName+".crt", certBytes); err != nil {
return nil, err
}

if err := manager.Cache.Put(ctx, serverName+".key", keyBytes); err != nil {
return nil, err
}

logging.L.Info().
Str("serverName", serverName).
Str("fingerprint", Fingerprint(pemDecode(certBytes))).
Msg("new server certificate")
}

keypair, err := tls.X509KeyPair(certBytes, keyBytes)
if err != nil {
return nil, err
}

return &keypair, nil
}
}

// Fingerprint returns a sha256 checksum of the certificate formatted as
// hex pairs separated by colons. This is a common format used by browsers.
// The bytes must be the ASN.1 DER form of the x509.Certificate.
Expand All @@ -133,11 +66,6 @@ func Fingerprint(raw []byte) string {
return strings.ToUpper(s)
}

func pemDecode(raw []byte) []byte {
block, _ := pem.Decode(raw)
return block.Bytes
}

// PEMEncodeCertificate accepts the bytes of a x509 certificate in ASN.1 DER form
// and returns a PEM encoded representation of that certificate.
func PEMEncodeCertificate(raw []byte) []byte {
Expand All @@ -149,39 +77,3 @@ func PEMEncodeCertificate(raw []byte) []byte {
func pemEncodePrivateKey(raw []byte) []byte {
return pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: raw})
}

func newCA() (*x509.Certificate, *rsa.PrivateKey, error) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No longer used, we'll rely on helm or the user to provide the CA.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What implications does this have on the initial deploy (ex: quickstart)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, good question. I added another TODO to the list in the description. We need to generate the CA in helm as part of the deploy. That should make it invisible to the user. They won't have to do anything new, but helm will take care of creating the secret which contains the CA and CA private key.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've pushed a commit which does this (generates the CA in helm and stores it in a secret)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The other implication is that the server can no longer be served outside of a helm install, e.g. docker, compose, or local build, unless the CA is manually created and configured. It'll be nice to keep this to support those use cases but not critical

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ya, that's true. We could keep the ability to generate a CA if none is provided for development and demo purposes. I think in practice no real production use case should have the server generate a CA on first install. The operator really needs to save that CA and private key somewhere, and they aren't going to know it exists or where to find it if it was generated on first start.

I'll see about re-adding this for development.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One of the disadvantages to generating the CA by default is that if the user accidentally mis-configured either ca or caPrivateKey. For example, maybe they set only one of the two, or they set primaryKey instead of caPrimaryKey. With the changes in this PR they will get a nice error and know how to fix it.

If we always generate things for them by default then it won't fail, and they'll have a harder time debugging.

I think it might be better to require these are set ahead of starting the server. For dev maybe we could add a --dev flag and do some setup in the CLI before starting the server.

Copy link
Collaborator

@mxyng mxyng Jul 5, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Either Helm or infra can exit error if only one of ca or caPrivateKey is supplied since both are required. It'll reduce the impact of the scenario you're suggesting. Either way, it can be addressed in a future change

// Generate a CA to sign self-signed certificates
serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
if err != nil {
return nil, nil, err
}

caPrivKey, err := rsa.GenerateKey(rand.Reader, 4096)
if err != nil {
return nil, nil, err
}

ca := &x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
Organization: []string{"Infra"},
},
NotBefore: time.Now().Add(-5 * time.Minute).UTC(),
NotAfter: time.Now().AddDate(0, 0, 365).UTC(),
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageCertSign,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
IsCA: true,
BasicConstraintsValid: true,
}

caBytes, err := x509.CreateCertificate(rand.Reader, ca, ca, &caPrivKey.PublicKey, caPrivKey)
if err != nil {
return nil, nil, err
}

// TODO: is there really no other way to get the Raw field populated?
ca, _ = x509.ParseCertificate(caBytes)

return ca, caPrivKey, nil
}
3 changes: 1 addition & 2 deletions internal/cmd/list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,13 @@ func TestListCmd(t *testing.T) {
{User: "[email protected]", Resource: "moon", Role: "inhabitant"},
},
}
opts.Addr = server.ListenerOptions{HTTPS: "127.0.0.1:0", HTTP: "127.0.0.1:0"}
setupServerTLSOptions(t, &opts)
srv, err := server.New(opts)
assert.NilError(t, err)

ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)

setupCertManager(t, opts.TLSCache, srv.Addrs.HTTPS.String())
go func() {
assert.Check(t, srv.Run(ctx))
}()
Expand Down
33 changes: 9 additions & 24 deletions internal/cmd/login_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package cmd
import (
"context"
"encoding/pem"
"net"
"os"
"path/filepath"
"testing"
Expand All @@ -15,7 +14,6 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/hinshun/vt10x"
"golang.org/x/crypto/acme/autocert"
"golang.org/x/sync/errgroup"
"gotest.tools/v3/assert"
"gotest.tools/v3/assert/opt"
Expand All @@ -24,6 +22,7 @@ import (

"github.com/infrahq/infra/api"
"github.com/infrahq/infra/internal/certs"
"github.com/infrahq/infra/internal/cmd/types"
"github.com/infrahq/infra/internal/race"
"github.com/infrahq/infra/internal/server"
"github.com/infrahq/infra/uid"
Expand All @@ -35,18 +34,14 @@ func TestLoginCmd_SetupAdminOnFirstLogin(t *testing.T) {
dir := setupEnv(t)

opts := defaultServerOptions(dir)
opts.Addr = server.ListenerOptions{HTTPS: "127.0.0.1:0", HTTP: "127.0.0.1:0"}
setupServerTLSOptions(t, &opts)

srv, err := server.New(opts)
assert.NilError(t, err)

ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)

host, _, err := net.SplitHostPort(srv.Addrs.HTTPS.String())
assert.NilError(t, err)

setupCertManager(t, opts.TLSCache, host)
go func() {
assert.Check(t, srv.Run(ctx))
}()
Expand Down Expand Up @@ -133,7 +128,7 @@ func TestLoginCmd_Options(t *testing.T) {
dir := setupEnv(t)

opts := defaultServerOptions(dir)
opts.Addr = server.ListenerOptions{HTTPS: "127.0.0.1:0", HTTP: "127.0.0.1:0"}
setupServerTLSOptions(t, &opts)
adminAccessKey := "aaaaaaaaaa.bbbbbbbbbbbbbbbbbbbbbbbb"
opts.Config.Users = []server.User{
{
Expand All @@ -147,7 +142,6 @@ func TestLoginCmd_Options(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)

setupCertManager(t, opts.TLSCache, srv.Addrs.HTTPS.String())
go func() {
assert.Check(t, srv.Run(ctx))
}()
Expand Down Expand Up @@ -286,23 +280,18 @@ func setupEnv(t *testing.T) string {
return dir
}

// setupCertManager copies the static TLS cert and key into the cache that will
// be used by the server. This allows the server to skip generating a private key
// for both the CA and server certificate, which takes multiple seconds.
func setupCertManager(t *testing.T, dir string, serverName string) {
func setupServerTLSOptions(t *testing.T, opts *server.Options) {
t.Helper()
ctx := context.Background()
cache := autocert.DirCache(dir)

opts.Addr = server.ListenerOptions{HTTPS: "127.0.0.1:0", HTTP: "127.0.0.1:0"}

key, err := os.ReadFile("testdata/pki/localhost.key")
assert.NilError(t, err)
err = cache.Put(ctx, serverName+".key", key)
assert.NilError(t, err)
opts.TLS.PrivateKey = string(key)

cert, err := os.ReadFile("testdata/pki/localhost.crt")
assert.NilError(t, err)
err = cache.Put(ctx, serverName+".crt", cert)
assert.NilError(t, err)
opts.TLS.Certificate = types.StringOrFile(cert)
}

func TestLoginCmd_TLSVerify(t *testing.T) {
Expand All @@ -314,21 +303,17 @@ func TestLoginCmd_TLSVerify(t *testing.T) {
t.Setenv("KUBECONFIG", kubeConfigPath)

opts := defaultServerOptions(dir)
setupServerTLSOptions(t, &opts)
accessKey := "0000000001.adminadminadminadmin1234"
opts.Users = []server.User{
{Name: "[email protected]", AccessKey: accessKey},
}
opts.Addr = server.ListenerOptions{HTTPS: "127.0.0.1:0", HTTP: "127.0.0.1:0"}
srv, err := server.New(opts)
assert.NilError(t, err)

ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)

host, _, err := net.SplitHostPort(srv.Addrs.HTTPS.String())
assert.NilError(t, err)

setupCertManager(t, opts.TLSCache, host)
go func() {
assert.Check(t, srv.Run(ctx))
}()
Expand Down
7 changes: 7 additions & 0 deletions internal/cmd/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ tls:
caPrivateKey: file:ca.key
certificate: testdata/server.crt
privateKey: file:server.key
ACME: true

keys:
- kind: vault
Expand Down Expand Up @@ -223,6 +224,7 @@ users:
CAPrivateKey: "file:ca.key",
Certificate: "-----BEGIN CERTIFICATE-----\nnot a real server certificate\n-----END CERTIFICATE-----\n",
PrivateKey: "file:server.key",
ACME: true,
},

Keys: []server.KeyProvider{
Expand Down Expand Up @@ -319,6 +321,11 @@ func TestServerCmd_WithSecretsConfig(t *testing.T) {
http: "127.0.0.1:0"
https: "127.0.0.1:0"
metrics: "127.0.0.1:0"

tls:
ca: some-ca
caPrivateKey: some-key

secrets:
- kind: env
name: base64env
Expand Down
Loading