зеркало из https://github.com/Azure/draft-classic.git
feat/tls: enable draft/draftd TLS
This commit is contained in:
Родитель
095204f1e1
Коммит
f68a574f81
|
@ -16,6 +16,7 @@ import (
|
|||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
"k8s.io/helm/pkg/kube"
|
||||
"k8s.io/helm/pkg/tlsutil"
|
||||
|
||||
"github.com/Azure/draft/pkg/draft"
|
||||
"github.com/Azure/draft/pkg/draft/draftpath"
|
||||
|
@ -41,6 +42,16 @@ var (
|
|||
draftHost string
|
||||
// draftNamespace depicts which namespace the Draftd server is running in. This is used when Draftd was installed in a different namespace than kube-system.
|
||||
draftNamespace string
|
||||
// tls flags, options, and defaults
|
||||
tlsCaCertFile string // path to TLS CA certificate file
|
||||
tlsCertFile string // path to TLS certificate file
|
||||
tlsKeyFile string // path to TLS key file
|
||||
tlsVerify bool // enable TLS and verify remote certificates
|
||||
tlsEnable bool // enable TLS
|
||||
|
||||
tlsCaCertDefault = "$DRAFT_HOME/ca.pem"
|
||||
tlsCertDefault = "$DRAFT_HOME/cert.pem"
|
||||
tlsKeyDefault = "$DRAFT_HOME/key.pem"
|
||||
)
|
||||
|
||||
var globalUsage = `The application deployment tool for Kubernetes.
|
||||
|
@ -56,6 +67,7 @@ func newRootCmd(out io.Writer, in io.Reader) *cobra.Command {
|
|||
if flagDebug {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
}
|
||||
os.Setenv(homeEnvVar, draftHome)
|
||||
},
|
||||
PersistentPostRun: func(cmd *cobra.Command, args []string) {
|
||||
teardown()
|
||||
|
@ -72,8 +84,8 @@ func newRootCmd(out io.Writer, in io.Reader) *cobra.Command {
|
|||
newCreateCmd(out),
|
||||
newHomeCmd(out),
|
||||
newInitCmd(out, in),
|
||||
newUpCmd(out),
|
||||
newVersionCmd(out),
|
||||
addFlagsTLS(newUpCmd(out)),
|
||||
addFlagsTLS(newVersionCmd(out)),
|
||||
newPluginCmd(out),
|
||||
newConnectCmd(out),
|
||||
newDeleteCmd(out),
|
||||
|
@ -116,14 +128,43 @@ func teardown() {
|
|||
}
|
||||
|
||||
func ensureDraftClient(client *draft.Client) *draft.Client {
|
||||
if client == nil {
|
||||
return draft.NewClient(&draft.ClientConfig{
|
||||
ServerAddr: draftHost,
|
||||
Stdout: os.Stdout,
|
||||
Stderr: os.Stderr,
|
||||
})
|
||||
if client != nil {
|
||||
return client
|
||||
}
|
||||
return client
|
||||
return newClient()
|
||||
}
|
||||
|
||||
func newClient() *draft.Client {
|
||||
cfg := &draft.ClientConfig{ServerAddr: draftHost, Stdout: os.Stdout, Stderr: os.Stderr}
|
||||
if tlsVerify || tlsEnable {
|
||||
if tlsCaCertFile == tlsCaCertDefault {
|
||||
tlsCaCertFile = os.ExpandEnv(tlsCaCertDefault)
|
||||
}
|
||||
if tlsCertFile == tlsCertDefault {
|
||||
tlsCertFile = os.ExpandEnv(tlsCertDefault)
|
||||
}
|
||||
if tlsKeyFile == tlsKeyDefault {
|
||||
tlsKeyFile = os.ExpandEnv(tlsKeyDefault)
|
||||
}
|
||||
debug("Key=%q, Cert=%q, CA=%q\n", tlsKeyFile, tlsCertFile, tlsCaCertFile)
|
||||
tlsopts := tlsutil.Options{
|
||||
InsecureSkipVerify: true,
|
||||
CertFile: tlsCertFile,
|
||||
KeyFile: tlsKeyFile,
|
||||
}
|
||||
if tlsVerify {
|
||||
tlsopts.InsecureSkipVerify = false
|
||||
tlsopts.CaCertFile = tlsCaCertFile
|
||||
}
|
||||
tlscfg, err := tlsutil.ClientConfig(tlsopts)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(2)
|
||||
}
|
||||
cfg.UseTLS = true
|
||||
cfg.TLSConfig = tlscfg
|
||||
}
|
||||
return draft.NewClient(cfg)
|
||||
}
|
||||
|
||||
func defaultDraftHost() string {
|
||||
|
@ -189,3 +230,15 @@ func validateArgs(args, expectedArgs []string) error {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// addFlagsTLS adds the flags for supporting client side TLS to the
|
||||
// helm command (only those that invoke communicate to Draftd.)
|
||||
func addFlagsTLS(cmd *cobra.Command) *cobra.Command {
|
||||
// add flags
|
||||
cmd.Flags().StringVar(&tlsCaCertFile, "tls-ca-cert", tlsCaCertDefault, "path to TLS CA certificate file")
|
||||
cmd.Flags().StringVar(&tlsCertFile, "tls-cert", tlsCertDefault, "path to TLS certificate file")
|
||||
cmd.Flags().StringVar(&tlsKeyFile, "tls-key", tlsKeyDefault, "path to TLS key file")
|
||||
cmd.Flags().BoolVar(&tlsVerify, "tls-verify", false, "enable TLS for request and verify remote")
|
||||
cmd.Flags().BoolVar(&tlsEnable, "tls", false, "enable TLS for request")
|
||||
return cmd
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
|
@ -37,15 +36,6 @@ $DRAFT_HOME, but not attempt to connect to a remote cluster and install the Draf
|
|||
deployment.
|
||||
|
||||
To dump information about the Draft chart, combine the '--dry-run' and '--debug' flags.
|
||||
`
|
||||
chartConfigTpl = `
|
||||
ingress:
|
||||
enabled: %s
|
||||
basedomain: %s
|
||||
registry:
|
||||
url: %s
|
||||
authtoken: %s
|
||||
imageOverride: %s
|
||||
`
|
||||
)
|
||||
|
||||
|
@ -86,10 +76,41 @@ func newInitCmd(out io.Writer, in io.Reader) *cobra.Command {
|
|||
f.BoolVar(&i.autoAccept, "auto-accept", false, "automatically accept configuration defaults (if detected). It will still prompt for information if this is set to true and no cloud provider was found")
|
||||
f.BoolVar(&i.dryRun, "dry-run", false, "go through all the steps without actually installing anything. Mostly used along with --debug for debugging purposes.")
|
||||
f.StringVarP(&i.image, "draftd-image", "i", "", "override Draftd image")
|
||||
// flags for bootstrapping draftd with tls
|
||||
f.BoolVar(&tlsEnable, "draftd-tls", false, "install Draftd with TLS enabled")
|
||||
f.BoolVar(&tlsVerify, "draftd-tls-verify", false, "install Draftd with TLS enabled and to verify remote certificates")
|
||||
f.StringVar(&tlsKeyFile, "draftd-tls-key", "", "path to TLS key file to install with Draftd")
|
||||
f.StringVar(&tlsCertFile, "draftd-tls-cert", "", "path to TLS certificate file to install with Draftd")
|
||||
f.StringVar(&tlsCaCertFile, "tls-ca-cert", "", "path to CA root certificate")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// tlsOptions sanitizes the tls flags as well as checks for the existence of
|
||||
// required tls files indicate by those flags, if any.
|
||||
func (i *initCmd) tlsOptions(cfg *installerConfig.DraftConfig) error {
|
||||
cfg.EnableTLS = tlsEnable || tlsVerify
|
||||
cfg.VerifyTLS = tlsVerify
|
||||
if cfg.EnableTLS {
|
||||
missing := func(file string) bool {
|
||||
_, err := os.Stat(file)
|
||||
return os.IsNotExist(err)
|
||||
}
|
||||
if cfg.TLSKeyFile = tlsKeyFile; cfg.TLSKeyFile == "" || missing(cfg.TLSKeyFile) {
|
||||
return errors.New("missing required TLS key file")
|
||||
}
|
||||
if cfg.TLSCertFile = tlsCertFile; cfg.TLSCertFile == "" || missing(cfg.TLSCertFile) {
|
||||
return errors.New("missing required TLS certificate file")
|
||||
}
|
||||
if cfg.VerifyTLS {
|
||||
if cfg.TLSCaCertFile = tlsCaCertFile; cfg.TLSCaCertFile == "" || missing(cfg.TLSCaCertFile) {
|
||||
return errors.New("missing required TLS CA file")
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// runInit initializes local config and installs Draft to Kubernetes Cluster
|
||||
func (i *initCmd) run() error {
|
||||
|
||||
|
@ -155,15 +176,18 @@ func (i *initCmd) run() error {
|
|||
|
||||
}
|
||||
|
||||
rawChartConfig := fmt.Sprintf(chartConfigTpl, strconv.FormatBool(draftConfig.Ingress), draftConfig.Basedomain, draftConfig.RegistryURL, draftConfig.RegistryAuth, draftConfig.Image)
|
||||
log.Debugf("raw chart config: %s", rawChartConfig)
|
||||
if err := i.tlsOptions(draftConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debugf("raw chart config: %s", draftConfig)
|
||||
|
||||
if !i.dryRun {
|
||||
// attempt to purge the old release, but log errors to --debug
|
||||
if err := installer.Uninstall(i.helmClient); err != nil {
|
||||
log.Debugf("error uninstalling Draft: %s", err)
|
||||
}
|
||||
if err := installer.Install(i.helmClient, draftNamespace, rawChartConfig); err != nil {
|
||||
if err := installer.Install(i.helmClient, draftNamespace, draftConfig); err != nil {
|
||||
return fmt.Errorf("error installing Draft: %s", err)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,67 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
b64 "encoding/base64"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
"os"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const chartConfigTpl = `
|
||||
ingress:
|
||||
enabled: %s
|
||||
basedomain: %s
|
||||
registry:
|
||||
url: %s
|
||||
authtoken: %s
|
||||
imageOverride: %s
|
||||
`
|
||||
|
||||
const chartConfigTLSTpl = `
|
||||
tls:
|
||||
enable: %s
|
||||
verify: %s
|
||||
key: %s
|
||||
cert: %s
|
||||
cacert: %s
|
||||
`
|
||||
|
||||
type DraftConfig struct {
|
||||
Basedomain string
|
||||
Image string
|
||||
Ingress bool
|
||||
RegistryURL string
|
||||
RegistryAuth string
|
||||
|
||||
//
|
||||
// TLS configurations for Draftd
|
||||
//
|
||||
|
||||
// EnableTLS instructs Draftd to serve with TLS enabled.
|
||||
//
|
||||
// Implied by VerifyTLS. If set the TLSKey and TLSCert are required.
|
||||
EnableTLS bool
|
||||
// VerifyTLS instructs Draftd to serve with TLS enabled verify remote certificates.
|
||||
//
|
||||
// If set TLSKey, TLSCert, TLSCaCert are required.
|
||||
VerifyTLS bool
|
||||
// TLSKeyFile identifies the file containing the pem encoded TLS private
|
||||
// key Draftd should use.
|
||||
//
|
||||
// Required and valid if and only if EnableTLS or VerifyTLS is set.
|
||||
TLSKeyFile string
|
||||
// TLSCertFile identifies the file containing the pem encoded TLS
|
||||
// certificate Draftd should use.
|
||||
//
|
||||
// Required and valid if and only if EnableTLS or VerifyTLS is set.
|
||||
TLSCertFile string
|
||||
// TLSCaCertFile identifies the file containing the pem encoded TLS CA
|
||||
// certificate Draftd should use to verify remotes certificates.
|
||||
//
|
||||
// Required and valid if and only if VerifyTLS is set.
|
||||
TLSCaCertFile string
|
||||
}
|
||||
|
||||
// FromClientConfig reads a kubernetes client config, searching for information that may indicate
|
||||
|
@ -36,3 +88,40 @@ func FromClientConfig(config clientcmd.ClientConfig) (*DraftConfig, string, erro
|
|||
|
||||
return draftConfig, cloudProviderName, nil
|
||||
}
|
||||
|
||||
func (cfg *DraftConfig) WithTLS() bool { return cfg.EnableTLS || cfg.VerifyTLS }
|
||||
|
||||
// String returns the string representation of a DraftConfig.
|
||||
func (cfg *DraftConfig) String() string {
|
||||
cfgstr := fmt.Sprintf(
|
||||
chartConfigTpl,
|
||||
strconv.FormatBool(cfg.Ingress),
|
||||
cfg.Basedomain,
|
||||
cfg.RegistryURL,
|
||||
cfg.RegistryAuth,
|
||||
cfg.Image,
|
||||
)
|
||||
if cfg.WithTLS() {
|
||||
cfgstr += addChartConfigTLSTpl(cfg)
|
||||
}
|
||||
return cfgstr
|
||||
}
|
||||
|
||||
func addChartConfigTLSTpl(cfg *DraftConfig) string {
|
||||
return fmt.Sprintf(chartConfigTLSTpl,
|
||||
strconv.FormatBool(cfg.EnableTLS),
|
||||
strconv.FormatBool(cfg.VerifyTLS),
|
||||
b64.StdEncoding.EncodeToString(readfile(cfg.TLSKeyFile)),
|
||||
b64.StdEncoding.EncodeToString(readfile(cfg.TLSCertFile)),
|
||||
b64.StdEncoding.EncodeToString(readfile(cfg.TLSCaCertFile)),
|
||||
)
|
||||
}
|
||||
|
||||
func readfile(path string) []byte {
|
||||
b, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "fatal: reading file %q: %v\n", path, err)
|
||||
os.Exit(2)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"k8s.io/helm/pkg/helm"
|
||||
"k8s.io/helm/pkg/proto/hapi/chart"
|
||||
|
||||
draftconfig "github.com/Azure/draft/cmd/draft/installer/config"
|
||||
"github.com/Azure/draft/pkg/version"
|
||||
)
|
||||
|
||||
|
@ -54,6 +55,12 @@ registry:
|
|||
#
|
||||
# $ echo '{"registrytoken":"9cbaf023786cd7"}' | base64 -w 0
|
||||
authtoken: e30K
|
||||
tls:
|
||||
enable: false
|
||||
verify: false
|
||||
key: ""
|
||||
cert: ""
|
||||
cacert: ""
|
||||
`
|
||||
|
||||
const draftIgnore = `# Patterns to ignore when building packages.
|
||||
|
@ -104,6 +111,8 @@ spec:
|
|||
- --registry-auth={{ .Values.registry.authtoken }}
|
||||
- --basedomain={{ .Values.ingress.basedomain }}
|
||||
- --ingress-enabled={{.Values.ingress.enabled}}
|
||||
- --tls={{.Values.tls.enable}}
|
||||
- --tls-verify={{.Values.tls.verify}}
|
||||
{{- if .Values.debug }}
|
||||
- --debug
|
||||
{{- end }}
|
||||
|
@ -114,6 +123,12 @@ spec:
|
|||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.namespace
|
||||
- name: DRAFT_TLS_VERIFY
|
||||
value: {{ if .Values.tls.verify }}"1"{{ else }}"0"{{ end }}
|
||||
- name: DRAFT_TLS_ENABLE
|
||||
value: {{ if .Values.tls.enable }}"1"{{ else }}"0"{{ end }}
|
||||
- name: DRAFT_TLS_CERTS
|
||||
value: "/etc/certs"
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /ping
|
||||
|
@ -125,10 +140,20 @@ spec:
|
|||
volumeMounts:
|
||||
- mountPath: /var/run/docker.sock
|
||||
name: docker-socket
|
||||
{{- if (or .Values.tls.enable .Values.tls.verify) }}
|
||||
- mountPath: "/etc/certs"
|
||||
name: draftd-certs
|
||||
readOnly: true
|
||||
{{- end }}
|
||||
volumes:
|
||||
- name: docker-socket
|
||||
hostPath:
|
||||
path: /var/run/docker.sock
|
||||
{{- if (or .Values.tls.enable .Values.tls.verify) }}
|
||||
- name: draftd-certs
|
||||
secret:
|
||||
secretName: draftd-secret
|
||||
{{- end }}
|
||||
nodeSelector:
|
||||
beta.kubernetes.io/os: linux
|
||||
`
|
||||
|
@ -146,6 +171,17 @@ spec:
|
|||
app: {{ .Chart.Name }}
|
||||
`
|
||||
|
||||
const draftSecret = `apiVersion: v1
|
||||
kind: Secret
|
||||
type: Opaque
|
||||
data:
|
||||
tls.key: {{ .Values.tls.key }}
|
||||
tls.crt: {{ .Values.tls.cert }}
|
||||
ca.crt: {{ .Values.tls.cacert }}
|
||||
metadata:
|
||||
name: draftd-secret
|
||||
`
|
||||
|
||||
const draftNotes = `Now you can deploy an app using Draft!
|
||||
|
||||
$ cd my-app
|
||||
|
@ -197,7 +233,15 @@ var DefaultChartFiles = []*chartutil.BufferedFile{
|
|||
// Install uses the helm client to install Draftd with the given config.
|
||||
//
|
||||
// Returns an error if the command failed.
|
||||
func Install(client *helm.Client, namespace string, rawChartConfig string) error {
|
||||
func Install(client *helm.Client, namespace string, config *draftconfig.DraftConfig) error {
|
||||
if config.WithTLS() {
|
||||
DefaultChartFiles = append(DefaultChartFiles, &chartutil.BufferedFile{
|
||||
// TODO: Add chartutil.SecretName to k8s.io/helm/pkg/chartutil/create.go
|
||||
Name: path.Join(chartutil.TemplatesDir, "secret.yaml"),
|
||||
Data: []byte(draftSecret),
|
||||
})
|
||||
}
|
||||
|
||||
chart, err := chartutil.LoadFiles(DefaultChartFiles)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -206,7 +250,8 @@ func Install(client *helm.Client, namespace string, rawChartConfig string) error
|
|||
chart,
|
||||
namespace,
|
||||
helm.ReleaseName(ReleaseName),
|
||||
helm.ValueOverrides([]byte(rawChartConfig)))
|
||||
helm.ValueOverrides([]byte(config.String())),
|
||||
)
|
||||
return prettyError(err)
|
||||
}
|
||||
|
||||
|
|
|
@ -5,20 +5,68 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/helm/pkg/tlsutil"
|
||||
)
|
||||
|
||||
const (
|
||||
// tlsEnableEnvVar names the environment variable that enables TLS.
|
||||
tlsEnableEnvVar = "TILLER_TLS_ENABLE"
|
||||
// tlsVerifyEnvVar names the environment variable that enables
|
||||
// TLS, as well as certificate verification of the remote peer.
|
||||
tlsVerifyEnvVar = "TILLER_TLS_VERIFY"
|
||||
// tlsCertsEnvVar names the environment variable that points to
|
||||
// the directory where Draftd's TLS certificates are located.
|
||||
tlsCertsEnvVar = "TILLER_TLS_CERTS"
|
||||
)
|
||||
|
||||
var (
|
||||
// flagDebug is a signal that the user wants additional output.
|
||||
flagDebug bool
|
||||
// tls flags and options
|
||||
tlsEnable bool
|
||||
tlsVerify bool
|
||||
keyFile string
|
||||
certFile string
|
||||
caCertFile string
|
||||
)
|
||||
|
||||
var globalUsage = "The draft server."
|
||||
|
||||
func tlsEnableEnvVarDefault() bool { return os.Getenv(tlsEnableEnvVar) != "" }
|
||||
func tlsVerifyEnvVarDefault() bool { return os.Getenv(tlsVerifyEnvVar) != "" }
|
||||
|
||||
func tlsDefaultsFromEnv(name string) (value string) {
|
||||
switch certsDir := os.Getenv(tlsCertsEnvVar); name {
|
||||
case "tls-key":
|
||||
return filepath.Join(certsDir, "tls.key")
|
||||
case "tls-cert":
|
||||
return filepath.Join(certsDir, "tls.crt")
|
||||
case "tls-ca-cert":
|
||||
return filepath.Join(certsDir, "ca.crt")
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func tlsOptions() tlsutil.Options {
|
||||
opts := tlsutil.Options{CertFile: certFile, KeyFile: keyFile}
|
||||
if tlsVerify {
|
||||
opts.CaCertFile = caCertFile
|
||||
// We want to force the client to not only provide a cert, but to
|
||||
// provide a cert that we can validate.
|
||||
//
|
||||
// See: http://www.bite-code.com/2015/06/25/tls-mutual-auth-in-golang/
|
||||
opts.ClientAuth = tls.RequireAndVerifyClientCert
|
||||
}
|
||||
return opts
|
||||
}
|
||||
|
||||
func newRootCmd(out io.Writer) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "draftd",
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"github.com/spf13/cobra"
|
||||
"golang.org/x/net/context"
|
||||
"k8s.io/helm/pkg/helm"
|
||||
"k8s.io/helm/pkg/tlsutil"
|
||||
|
||||
"github.com/Azure/draft/pkg/draft"
|
||||
"github.com/Azure/draft/pkg/kube"
|
||||
|
@ -71,6 +72,12 @@ func newStartCmd(out io.Writer) *cobra.Command {
|
|||
f.StringVar(&sc.tillerURI, "tiller-uri", "tiller-deploy:44134", "the URI used to connect to tiller")
|
||||
f.BoolVarP(&sc.local, "local", "", false, "run draftd locally (uses local kubecfg)")
|
||||
f.BoolVarP(&sc.ingressEnabled, "ingress-enabled", "", false, "configure ingress")
|
||||
// add TLS flags
|
||||
f.BoolVar(&tlsEnable, "tls", tlsEnableEnvVarDefault(), "enable TLS")
|
||||
f.BoolVar(&tlsVerify, "tls-verify", tlsVerifyEnvVarDefault(), "enable TLS and verify remote certificate")
|
||||
f.StringVar(&keyFile, "tls-key", tlsDefaultsFromEnv("tls-key"), "path to TLS private key file")
|
||||
f.StringVar(&certFile, "tls-cert", tlsDefaultsFromEnv("tls-cert"), "path to TLS certificate file")
|
||||
f.StringVar(&caCertFile, "tls-ca-cert", tlsDefaultsFromEnv("tls-ca-cert"), "trust certificates signed by this CA")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
@ -104,9 +111,18 @@ func (c *startCmd) run() (err error) {
|
|||
return fmt.Errorf("failed to create in-cluster kubernetes client: %v", err)
|
||||
}
|
||||
}
|
||||
if tlsEnable || tlsVerify {
|
||||
tlscfg, err := tlsutil.ServerConfig(tlsOptions())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create server TLS configuration: %v", err)
|
||||
}
|
||||
cfg.UseTLS = true
|
||||
cfg.TLSConfig = tlscfg
|
||||
}
|
||||
|
||||
cfg.Helm = helm.NewClient(helm.Host(c.tillerURI))
|
||||
log.Printf("server is now listening at %s", c.listenAddr)
|
||||
|
||||
log.Printf("server is now listening at %s (tls=%t)", c.listenAddr, tlsEnable || tlsVerify)
|
||||
|
||||
return draft.NewServer(cfg).Serve(context.Background())
|
||||
}
|
||||
|
|
|
@ -0,0 +1,249 @@
|
|||
# Using SSL Between Draft and Draftd
|
||||
|
||||
This document explains how to create strong SSL/TLS connections between the
|
||||
Draft client (Draft) and server (Draftd). The emphasis here is on creating an
|
||||
internal CA, and using both the cryptographic and identity functions of SSL.
|
||||
|
||||
Configuring SSL is considered an advanced topic, and knowledge of Draft is assumed.
|
||||
|
||||
## Overview
|
||||
|
||||
The Draftd authentication model uses client-side SSL certificates. Draftd itself
|
||||
verifies these certificates using a certificate authority. Likewise, the client
|
||||
also verifies Draftd's identity by certificate authority.
|
||||
|
||||
NOTE: For now this document concerns configuring the Draft client and server to
|
||||
communicate using SSL certificates. Specifically, TLS is used to secure gRPC
|
||||
communication between the Draft client and server.
|
||||
|
||||
There are numerous possible configurations for setting up certificates and authorities,
|
||||
but the method we cover here will work for most situations.
|
||||
|
||||
In this guide, we will show how to:
|
||||
|
||||
- Create a private CA that is used to issue certificates for Draftd clients and
|
||||
servers.
|
||||
- Create a certificate for Draftd
|
||||
- Create a certificate for the Draft client
|
||||
- Create a Draftd instance that uses the certificate
|
||||
- Configure the Draft client to use the CA and client-side certificate
|
||||
|
||||
By the end of this guide, you should have a Draftd instance running that will
|
||||
only accept connections from clients who can be authenticated by SSL certificate.
|
||||
|
||||
## Generating Certificate Authorities and Certificates
|
||||
|
||||
One way to generate SSL CAs is via the `openssl` command line tool. There are many
|
||||
guides and best practices documents available online. This explanation is focused
|
||||
on getting ready within a small amount of time. For production configurations,
|
||||
we urge readers to read [the official documentation](https://www.openssl.org) and
|
||||
consult other resources.
|
||||
|
||||
### Generate a Certificate Authority
|
||||
|
||||
The simplest way to generate a certificate authority is to run two commands:
|
||||
|
||||
```console
|
||||
$ openssl genrsa -out ./ca.key.pem 4096
|
||||
$ openssl req -key ca.key.pem -new -x509 -days 7300 -sha256 -out ca.cert.pem -extensions v3_ca
|
||||
Enter pass phrase for ca.key.pem:
|
||||
You are about to be asked to enter information that will be incorporated
|
||||
into your certificate request.
|
||||
What you are about to enter is what is called a Distinguished Name or a DN.
|
||||
There are quite a few fields but you can leave some blank
|
||||
For some fields there will be a default value,
|
||||
If you enter '.', the field will be left blank.
|
||||
-----
|
||||
Country Name (2 letter code) [AU]:US
|
||||
State or Province Name (full name) [Some-State]:CO
|
||||
Locality Name (eg, city) []:Boulder
|
||||
Organization Name (eg, company) [Internet Widgits Pty Ltd]:draft
|
||||
Organizational Unit Name (eg, section) []:
|
||||
Common Name (e.g. server FQDN or YOUR name) []:draft
|
||||
Email Address []:draftd@example.com
|
||||
```
|
||||
|
||||
Note that the data input above is _sample data_. You should customize to your own
|
||||
specifications.
|
||||
|
||||
The above will generate both a secret key and a CA. Note that these two files are
|
||||
very important. The key in particular should be handled with particular care.
|
||||
|
||||
Often, you will want to generate an intermediate signing key. For the sake of brevity,
|
||||
we will be signing keys with our root CA.
|
||||
|
||||
### Generating Certificates
|
||||
|
||||
We will be generating two certificates, each representing a type of certificate:
|
||||
|
||||
- One certificate is for Draftd. You will want one of these _per draftd host_ that
|
||||
you run.
|
||||
- One certificate is for the user. You will want one of these _per draft client_.
|
||||
|
||||
Since the commands to generate these are the same, we'll be creating both at the
|
||||
same time. The names will indicate their target.
|
||||
|
||||
First, the Draftd key:
|
||||
|
||||
```console
|
||||
$ openssl genrsa -out ./draftd.key.pem 4096
|
||||
Generating RSA private key, 4096 bit long modulus
|
||||
..........................................................................................................................................................................................................................................................................................................................++
|
||||
............................................................................++
|
||||
e is 65537 (0x10001)
|
||||
Enter pass phrase for ./draftd.key.pem:
|
||||
Verifying - Enter pass phrase for ./draftd.key.pem:
|
||||
```
|
||||
|
||||
Next, generate the Draft client's key:
|
||||
|
||||
```console
|
||||
$ openssl genrsa -out ./draft.key.pem 4096
|
||||
Generating RSA private key, 4096 bit long modulus
|
||||
.....++
|
||||
......................................................................................................................................................................................++
|
||||
e is 65537 (0x10001)
|
||||
Enter pass phrase for ./draft.key.pem:
|
||||
Verifying - Enter pass phrase for ./draft.key.pem:
|
||||
```
|
||||
|
||||
Again, for production use you will generate one client certificate for each user.
|
||||
|
||||
Next we need to create certificates from these keys. For each certificate, this is
|
||||
a two-step process of creating a CSR, and then creating the certificate.
|
||||
|
||||
```console
|
||||
$ openssl req -key draftd.key.pem -new -sha256 -out draftd.csr.pem
|
||||
Enter pass phrase for draftd.key.pem:
|
||||
You are about to be asked to enter information that will be incorporated
|
||||
into your certificate request.
|
||||
What you are about to enter is what is called a Distinguished Name or a DN.
|
||||
There are quite a few fields but you can leave some blank
|
||||
For some fields there will be a default value,
|
||||
If you enter '.', the field will be left blank.
|
||||
-----
|
||||
Country Name (2 letter code) [AU]:US
|
||||
State or Province Name (full name) [Some-State]:CO
|
||||
Locality Name (eg, city) []:Boulder
|
||||
Organization Name (eg, company) [Internet Widgits Pty Ltd]:Draftd Server
|
||||
Organizational Unit Name (eg, section) []:
|
||||
Common Name (e.g. server FQDN or YOUR name) []:draftd-server
|
||||
Email Address []:
|
||||
|
||||
Please enter the following 'extra' attributes
|
||||
to be sent with your certificate request
|
||||
A challenge password []:
|
||||
An optional company name []:
|
||||
```
|
||||
|
||||
And we repeat this step for the Draft client certificate:
|
||||
|
||||
```console
|
||||
$ openssl req -key draft.key.pem -new -sha256 -out draft.csr.pem
|
||||
# Answer the questions with your client user's info
|
||||
```
|
||||
|
||||
(In rare cases, we've had to add the `-nodes` flag when generating the request.)
|
||||
|
||||
Now we sign each of these CSRs with the CA certificate we created:
|
||||
|
||||
```console
|
||||
$ openssl x509 -req -CA ca.cert.pem -CAkey ca.key.pem -CAcreateserial -in draftd.csr.pem -out draftd.cert.pem
|
||||
Signature ok
|
||||
subject=/C=US/ST=CO/L=Boulder/O=Draftd Server/CN=draftd-server
|
||||
Getting CA Private Key
|
||||
Enter pass phrase for ca.key.pem:
|
||||
```
|
||||
|
||||
And again for the client certificate:
|
||||
|
||||
```console
|
||||
$ openssl x509 -req -CA ca.cert.pem -CAkey ca.key.pem -CAcreateserial -in draft.csr.pem -out draft.cert.pem
|
||||
```
|
||||
|
||||
At this point, the important files for us are these:
|
||||
|
||||
```
|
||||
# The CA. Make sure the key is kept secret.
|
||||
ca.cert.pem
|
||||
ca.key.pem
|
||||
# The Draft client files
|
||||
draft.cert.pem
|
||||
draft.key.pem
|
||||
# The Draftd server files.
|
||||
draftd.cert.pem
|
||||
draftd.key.pem
|
||||
```
|
||||
|
||||
Now we're ready to move on to the next steps.
|
||||
|
||||
## Creating a Custom Draftd Installation
|
||||
|
||||
The Draft client includes full support for creating a deployment configured for SSL.
|
||||
By specifying a few flags, the `draft init` command can create a new Draftd installation
|
||||
complete with all of our SSL configuration.
|
||||
|
||||
To take a look at what this will generate, run this command:
|
||||
|
||||
```console
|
||||
$ draft init --dry-run --debug --draftd-tls --draftd-tls-cert ./draftd.cert.pem --draftd-tls-key ./draftd.key.pem --draftd-tls-verify --tls-ca-cert ca.cert.pem
|
||||
```
|
||||
|
||||
The output will show you the values file used to install the Draft chart. Your SSL
|
||||
information will be preloaded into the Secret, which the Deployment will mount to
|
||||
pods as they start up.
|
||||
|
||||
|
||||
## Configuring the Draft Client
|
||||
|
||||
The Draftd server is now running with TLS protection. It's time to configure the
|
||||
Draft client to enable TLS-secured operations.
|
||||
|
||||
For a quick test, we can specify our configuration manually.
|
||||
|
||||
```console
|
||||
draft up --tls --tls-ca-cert ca.cert.pem --tls-cert draft.cert.pem --tls-key draft.key.pem
|
||||
```
|
||||
|
||||
This configuration sends our client-side certificate to establish identity, uses
|
||||
the client key for encryption, and uses the CA certificate to validate the remote
|
||||
Draftd's identity.
|
||||
|
||||
Typing a line that that is cumbersome, though. The shortcut is to move the key,
|
||||
cert, and CA into `$DRAFT_HOME`:
|
||||
|
||||
```console
|
||||
$ cp ca.cert.pem $(draft home)/ca.pem
|
||||
$ cp draft.cert.pem $(draft home)/cert.pem
|
||||
$ cp draft.key.pem $(draft home)/key.pem
|
||||
```
|
||||
|
||||
With this, you can simply run `draft up --tls` to enable TLS.
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
*Running a command, I get `Error: transport is closing`*
|
||||
|
||||
This is almost always due to a configuration error in which the client is missing
|
||||
a certificate (`--tls-cert`) or the certificate is bad.
|
||||
|
||||
*I'm using a certificate, but get `Error: remote error: tls: bad certificate`*
|
||||
|
||||
This means that Draftd's CA cannot verify your certificate. In the examples above,
|
||||
we used a single CA to generate both the client and server certificates. In these
|
||||
examples, the CA has _signed_ the client's certificate. We then load that CA
|
||||
up to Draftd. So when the client certificate is sent to the server, Draftd
|
||||
checks the client certificate against the CA.
|
||||
|
||||
*If I use `--tls-verify` on the client, I get `Error: x509: certificate is valid for draftd-server, not localhost`*
|
||||
|
||||
If you plan to use `--tls-verify` on the client, you will need to make sure that
|
||||
the host name that Draft connects to matches the host name on the certificate. In
|
||||
some cases this is awkward, since Draft will connect over localhost, or the FQDN is
|
||||
not available for public resolution.
|
||||
|
||||
## References
|
||||
|
||||
https://github.com/denji/golang-tls
|
||||
https://www.openssl.org/docs/
|
||||
https://jamielinux.com/docs/openssl-certificate-authority/sign-server-and-client-certificates.html
|
|
@ -1,10 +1,14 @@
|
|||
package draft
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
|
||||
"github.com/Azure/draft/pkg/build"
|
||||
"github.com/Azure/draft/pkg/rpc"
|
||||
"github.com/Azure/draft/pkg/version"
|
||||
|
@ -18,6 +22,8 @@ type ClientConfig struct {
|
|||
ServerHost string
|
||||
Stdout io.Writer
|
||||
Stderr io.Writer
|
||||
UseTLS bool
|
||||
TLSConfig *tls.Config
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
|
@ -29,6 +35,13 @@ type Client struct {
|
|||
// NewClient takes ClientConfig and returns a Client
|
||||
func NewClient(cfg *ClientConfig) *Client {
|
||||
opts := []rpc.ClientOpt{rpc.WithServerAddr(cfg.ServerAddr)}
|
||||
switch {
|
||||
case cfg.UseTLS:
|
||||
creds := grpc.WithTransportCredentials(credentials.NewTLS(cfg.TLSConfig))
|
||||
opts = append(opts, rpc.WithGrpcDialOpt(creds))
|
||||
default:
|
||||
opts = append(opts, rpc.WithGrpcDialOpt(grpc.WithInsecure()))
|
||||
}
|
||||
return &Client{
|
||||
cfg: cfg,
|
||||
rpc: rpc.NewClient(opts...),
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package draft
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
@ -17,6 +18,7 @@ import (
|
|||
"github.com/docker/docker/pkg/term"
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
apiErrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
k8s "k8s.io/client-go/kubernetes"
|
||||
|
@ -36,6 +38,8 @@ type ServerConfig struct {
|
|||
Docker *docker.Client
|
||||
Helm helm.Interface
|
||||
Kube k8s.Interface
|
||||
UseTLS bool
|
||||
TLSConfig *tls.Config
|
||||
}
|
||||
|
||||
// Server is a draft Server.
|
||||
|
@ -67,8 +71,15 @@ func (s *Server) Serve(ctx context.Context) error {
|
|||
return err
|
||||
}
|
||||
|
||||
var opts []rpc.ServerOpt
|
||||
if s.cfg.UseTLS {
|
||||
opts = append(opts, rpc.WithGrpcServerOpt(
|
||||
grpc.Creds(credentials.NewTLS(s.cfg.TLSConfig)),
|
||||
))
|
||||
}
|
||||
|
||||
errc := make(chan error, 1)
|
||||
s.srv = rpc.NewServer()
|
||||
s.srv = rpc.NewServer(opts...)
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
errc <- s.srv.Serve(lis, s)
|
||||
|
|
|
@ -15,6 +15,7 @@ type clientImpl struct {
|
|||
|
||||
func newClientImpl(opts ...ClientOpt) Client {
|
||||
var c clientImpl
|
||||
opts = append(DefaultClientOpts(), opts...)
|
||||
for _, opt := range opts {
|
||||
opt(&c.opts)
|
||||
}
|
||||
|
@ -164,8 +165,8 @@ func up_stream(ctx context.Context, rpc DraftClient, send <-chan *UpRequest, rec
|
|||
}
|
||||
|
||||
// connect connects the DraftClient to the DraftServer.
|
||||
func connect(c *clientImpl, opts ...grpc.DialOption) (conn *grpc.ClientConn, err error) {
|
||||
if conn, err = grpc.Dial(c.opts.addr, grpc.WithInsecure()); err != nil {
|
||||
func connect(c *clientImpl) (conn *grpc.ClientConn, err error) {
|
||||
if conn, err = grpc.Dial(c.opts.addr, c.opts.dialOpts...); err != nil {
|
||||
return nil, fmt.Errorf("failed to dial %q: %v", c.opts.addr, err)
|
||||
}
|
||||
return conn, nil
|
||||
|
|
|
@ -1,11 +1,18 @@
|
|||
package rpc
|
||||
|
||||
import (
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/keepalive"
|
||||
"time"
|
||||
)
|
||||
|
||||
// clientOpts specifies the union of all configurable
|
||||
// options an rpc.Client accepts to communicate with
|
||||
// the draft rpc.Server.
|
||||
type clientOpts struct {
|
||||
addr string
|
||||
host string
|
||||
dialOpts []grpc.DialOption
|
||||
addr string
|
||||
host string
|
||||
}
|
||||
|
||||
// WithServerAddr sets the draft server address
|
||||
|
@ -24,6 +31,46 @@ func WithServerHost(host string) ClientOpt {
|
|||
}
|
||||
}
|
||||
|
||||
// WithGrpcDialOpt adds the provided grpc.DialOptions for the
|
||||
// rpc.Client to use when initializing the underlying grpc.Client.
|
||||
func WithGrpcDialOpt(dialOpts ...grpc.DialOption) ClientOpt {
|
||||
return func(opts *clientOpts) {
|
||||
opts.dialOpts = append(opts.dialOpts, dialOpts...)
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultClientOpts returns the set of default rpc ClientOpts the draft
|
||||
// client requires.
|
||||
func DefaultClientOpts() []ClientOpt {
|
||||
return []ClientOpt{
|
||||
WithGrpcDialOpt(
|
||||
grpc.WithKeepaliveParams(keepalive.ClientParameters{
|
||||
Time: time.Duration(30) * time.Second,
|
||||
}),
|
||||
grpc.WithTimeout(5*time.Second),
|
||||
grpc.WithBlock(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// serverOpts specifies the union of all configurable
|
||||
// options an rpc.Server accepts, e.g. TLS config.
|
||||
type serverOpts struct{}
|
||||
type serverOpts struct {
|
||||
grpcOpts []grpc.ServerOption
|
||||
}
|
||||
|
||||
// WithGrpcServerOpt adds the provided grpc.ServerOption
|
||||
// for the rpc.Server to use when initializing the underlying
|
||||
// grpc.Server.
|
||||
func WithGrpcServerOpt(grpcOpts ...grpc.ServerOption) ServerOpt {
|
||||
return func(opts *serverOpts) {
|
||||
opts.grpcOpts = append(opts.grpcOpts, grpcOpts...)
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultServerOpts returns the set of default rpc ServerOpts that draftd requires.
|
||||
func DefaultServerOpts() []ServerOpt {
|
||||
return []ServerOpt{
|
||||
WithGrpcServerOpt(grpc.MaxMsgSize(maxMsgSize)),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@ type serverImpl struct {
|
|||
|
||||
func newServerImpl(opts ...ServerOpt) *serverImpl {
|
||||
var s serverImpl
|
||||
opts = append(DefaultServerOpts(), opts...)
|
||||
for _, opt := range opts {
|
||||
opt(&s.opts)
|
||||
}
|
||||
|
@ -38,13 +39,9 @@ func newServerImpl(opts ...ServerOpt) *serverImpl {
|
|||
|
||||
// Server implements rpc.Server.Serve
|
||||
func (s *serverImpl) Serve(lis net.Listener, h Handler) error {
|
||||
var opts = []grpc.ServerOption{
|
||||
grpc.MaxMsgSize(maxMsgSize),
|
||||
}
|
||||
// TODO: If TLS load keys and such
|
||||
s.h = h
|
||||
s.lis = lis
|
||||
s.srv = grpc.NewServer(opts...)
|
||||
s.srv = grpc.NewServer(s.opts.grpcOpts...)
|
||||
RegisterDraftServer(s.srv, s)
|
||||
return s.srv.Serve(s.lis)
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче