Merge pull request #60728 from stealthybox/59766-kubeadm_selfhosted_etcd_tls

Automatic merge from submit-queue. If you want to cherry-pick this change to another branch, please follow the instructions <a href="https://github.com/kubernetes/community/blob/master/contributors/devel/cherry-picks.md">here</a>.

Add mTLS to kubeadm etcd liveness probe.

**What this PR does / why we need it**:
We switched etcd over to using mTLS, but the liveness probe is still using http.
Disabling the liveness probe allows etcd to continue operating.

The real fix isn't simple, because we need to generate a client certificate for healthchecking and update the probe to exec `etcdctl` like so: 
https://sourcegraph.com/github.com/coreos/etcd-operator/-/blob/pkg/util/k8sutil/pod_util.go#L71-89

~Working on patching this now.~
This PR now generates the healthcheck identity and updates the liveness probe to use it.

**Which issue(s) this PR fixes**
Fixes #59766
Fixes kubernetes/kubeadm#720

**Special notes for your reviewer**:
We should generate a client cert specifically for etcd health checks so that the apiserver certs can be revoked independently.
This will be stored in `/etc/kubernetes/pki/etcd/` so that we don't have to change the pod's hostMount.

**Release note**:
```release-note
NONE
```
This commit is contained in:
Kubernetes Submit Queue 2018-03-05 10:12:28 -08:00 committed by GitHub
commit d37460147e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 231 additions and 47 deletions

View File

@ -98,6 +98,13 @@ var (
If both files already exist, kubeadm skips the generation step and existing files will be used.
`+cmdutil.AlphaDisclaimer), kubeadmconstants.EtcdPeerCertName, kubeadmconstants.EtcdPeerKeyName)
etcdHealthcheckClientCertLongDesc = fmt.Sprintf(normalizer.LongDesc(`
Generates the client certificate for liveness probes to healthcheck etcd and the respective key,
and saves them into %s and %s files.
If both files already exist, kubeadm skips the generation step and existing files will be used.
`+cmdutil.AlphaDisclaimer), kubeadmconstants.EtcdHealthcheckClientCertName, kubeadmconstants.EtcdHealthcheckClientKeyName)
apiServerEtcdServerCertLongDesc = fmt.Sprintf(normalizer.LongDesc(`
Generates the client certificate for the API server to connect to etcd securely and the respective key,
and saves them into %s and %s files.
@ -206,6 +213,12 @@ func getCertsSubCommands(defaultKubernetesVersion string) []*cobra.Command {
long: etcdPeerCertLongDesc,
cmdFunc: certsphase.CreateEtcdPeerCertAndKeyFiles,
},
{
use: "etcd-healthcheck-client",
short: "Generates a client certificate for liveness probes to healthcheck etcd",
long: etcdHealthcheckClientCertLongDesc,
cmdFunc: certsphase.CreateEtcdHealthcheckClientCertAndKeyFiles,
},
{
use: "apiserver-etcd-client",
short: "Generates a client certificate for the API server to connect to etcd securely",

View File

@ -73,6 +73,21 @@ func TestCertsSubCommandsHasFlags(t *testing.T) {
{
command: "apiserver-kubelet-client",
},
{
command: "etcd-ca",
},
{
command: "etcd-server",
},
{
command: "etcd-peer",
},
{
command: "etcd-healthcheck-client",
},
{
command: "apiserver-etcd-client",
},
{
command: "sa",
},
@ -113,6 +128,16 @@ func TestSubCmdCertsCreateFilesWithFlags(t *testing.T) {
subCmds: []string{"ca", "apiserver", "apiserver-kubelet-client"},
expectedFiles: []string{kubeadmconstants.CACertName, kubeadmconstants.CAKeyName, kubeadmconstants.APIServerCertName, kubeadmconstants.APIServerKeyName, kubeadmconstants.APIServerKubeletClientCertName, kubeadmconstants.APIServerKubeletClientKeyName},
},
{
subCmds: []string{"etcd-ca", "etcd-server", "etcd-peer", "etcd-healthcheck-client", "apiserver-etcd-client"},
expectedFiles: []string{
kubeadmconstants.EtcdCACertName, kubeadmconstants.EtcdCAKeyName,
kubeadmconstants.EtcdServerCertName, kubeadmconstants.EtcdServerKeyName,
kubeadmconstants.EtcdPeerCertName, kubeadmconstants.EtcdPeerKeyName,
kubeadmconstants.EtcdHealthcheckClientCertName, kubeadmconstants.EtcdHealthcheckClientKeyName,
kubeadmconstants.APIServerEtcdClientCertName, kubeadmconstants.APIServerEtcdClientKeyName,
},
},
{
subCmds: []string{"sa"},
expectedFiles: []string{kubeadmconstants.ServiceAccountPrivateKeyName, kubeadmconstants.ServiceAccountPublicKeyName},
@ -204,6 +229,16 @@ func TestSubCmdCertsCreateFilesWithConfigFile(t *testing.T) {
subCmds: []string{"ca", "apiserver", "apiserver-kubelet-client"},
expectedFiles: []string{kubeadmconstants.CACertName, kubeadmconstants.CAKeyName, kubeadmconstants.APIServerCertName, kubeadmconstants.APIServerKeyName, kubeadmconstants.APIServerKubeletClientCertName, kubeadmconstants.APIServerKubeletClientKeyName},
},
{
subCmds: []string{"etcd-ca", "etcd-server", "etcd-peer", "etcd-healthcheck-client", "apiserver-etcd-client"},
expectedFiles: []string{
kubeadmconstants.EtcdCACertName, kubeadmconstants.EtcdCAKeyName,
kubeadmconstants.EtcdServerCertName, kubeadmconstants.EtcdServerKeyName,
kubeadmconstants.EtcdPeerCertName, kubeadmconstants.EtcdPeerKeyName,
kubeadmconstants.EtcdHealthcheckClientCertName, kubeadmconstants.EtcdHealthcheckClientKeyName,
kubeadmconstants.APIServerEtcdClientCertName, kubeadmconstants.APIServerEtcdClientKeyName,
},
},
{
subCmds: []string{"front-proxy-ca", "front-proxy-client"},
expectedFiles: []string{kubeadmconstants.FrontProxyCACertName, kubeadmconstants.FrontProxyCAKeyName, kubeadmconstants.FrontProxyClientCertName, kubeadmconstants.FrontProxyClientKeyName},

View File

@ -90,13 +90,22 @@ const (
// EtcdPeerCertCommonName defines etcd's peer certificate common name (CN)
EtcdPeerCertCommonName = "kube-etcd-peer"
// APIServerEtcdClientCertAndKeyBaseName defines etcd client certificate and key base name
// EtcdHealthcheckClientCertAndKeyBaseName defines etcd's healthcheck client certificate and key base name
EtcdHealthcheckClientCertAndKeyBaseName = "etcd/healthcheck-client"
// EtcdHealthcheckClientCertName defines etcd's healthcheck client certificate name
EtcdHealthcheckClientCertName = "etcd/healthcheck-client.crt"
// EtcdHealthcheckClientKeyName defines etcd's healthcheck client key name
EtcdHealthcheckClientKeyName = "etcd/healthcheck-client.key"
// EtcdHealthcheckClientCertCommonName defines etcd's healthcheck client certificate common name (CN)
EtcdHealthcheckClientCertCommonName = "kube-etcd-healthcheck-client"
// APIServerEtcdClientCertAndKeyBaseName defines apiserver's etcd client certificate and key base name
APIServerEtcdClientCertAndKeyBaseName = "apiserver-etcd-client"
// APIServerEtcdClientCertName defines etcd client certificate name
// APIServerEtcdClientCertName defines apiserver's etcd client certificate name
APIServerEtcdClientCertName = "apiserver-etcd-client.crt"
// APIServerEtcdClientKeyName defines etcd client key name
// APIServerEtcdClientKeyName defines apiserver's etcd client key name
APIServerEtcdClientKeyName = "apiserver-etcd-client.key"
// APIServerEtcdClientCertCommonName defines etcd client certificate common name (CN)
// APIServerEtcdClientCertCommonName defines apiserver's etcd client certificate common name (CN)
APIServerEtcdClientCertCommonName = "kube-apiserver-etcd-client"
// ServiceAccountKeyBaseName defines SA key base name

View File

@ -40,6 +40,7 @@ func CreatePKIAssets(cfg *kubeadmapi.MasterConfiguration) error {
CreateEtcdCACertAndKeyFiles,
CreateEtcdServerCertAndKeyFiles,
CreateEtcdPeerCertAndKeyFiles,
CreateEtcdHealthcheckClientCertAndKeyFiles,
CreateAPIServerEtcdClientCertAndKeyFiles,
CreateServiceAccountKeyAndPublicKeyFiles,
CreateFrontProxyCACertAndKeyFiles,
@ -190,6 +191,30 @@ func CreateEtcdPeerCertAndKeyFiles(cfg *kubeadmapi.MasterConfiguration) error {
)
}
// CreateEtcdHealthcheckClientCertAndKeyFiles create a new client certificate for liveness probes to healthcheck etcd
// If the etcd-healthcheck-client certificate and key file already exist in the target folder, they are used only if evaluated equal; otherwise an error is returned.
// It assumes the etcd CA certificate and key file exist in the CertificatesDir
func CreateEtcdHealthcheckClientCertAndKeyFiles(cfg *kubeadmapi.MasterConfiguration) error {
etcdCACert, etcdCAKey, err := loadCertificateAuthority(cfg.CertificatesDir, kubeadmconstants.EtcdCACertAndKeyBaseName)
if err != nil {
return err
}
etcdHealthcheckClientCert, etcdHealthcheckClientKey, err := NewEtcdHealthcheckClientCertAndKey(etcdCACert, etcdCAKey)
if err != nil {
return err
}
return writeCertificateFilesIfNotExist(
cfg.CertificatesDir,
kubeadmconstants.EtcdHealthcheckClientCertAndKeyBaseName,
etcdCACert,
etcdHealthcheckClientCert,
etcdHealthcheckClientKey,
)
}
// CreateAPIServerEtcdClientCertAndKeyFiles create a new client certificate for the apiserver calling etcd
// If the apiserver-etcd-client certificate and key file already exist in the target folder, they are used only if evaluated equal; otherwise an error is returned.
// It assumes the etcd CA certificate and key file exist in the CertificatesDir
@ -375,6 +400,22 @@ func NewEtcdPeerCertAndKey(cfg *kubeadmapi.MasterConfiguration, caCert *x509.Cer
return etcdPeerCert, etcdPeerKey, nil
}
// NewEtcdHealthcheckClientCertAndKey generate certificate for liveness probes to healthcheck etcd, signed by the given CA.
func NewEtcdHealthcheckClientCertAndKey(caCert *x509.Certificate, caKey *rsa.PrivateKey) (*x509.Certificate, *rsa.PrivateKey, error) {
config := certutil.Config{
CommonName: kubeadmconstants.EtcdHealthcheckClientCertCommonName,
Organization: []string{kubeadmconstants.MastersGroup},
Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
}
etcdHealcheckClientCert, etcdHealcheckClientKey, err := pkiutil.NewCertAndKey(caCert, caKey, config)
if err != nil {
return nil, nil, fmt.Errorf("failure while creating etcd healthcheck client key and certificate: %v", err)
}
return etcdHealcheckClientCert, etcdHealcheckClientKey, nil
}
// NewAPIServerEtcdClientCertAndKey generate certificate for the apiservers to connect to etcd securely, signed by the given CA.
func NewAPIServerEtcdClientCertAndKey(caCert *x509.Certificate, caKey *rsa.PrivateKey) (*x509.Certificate, *rsa.PrivateKey, error) {

View File

@ -382,6 +382,22 @@ func TestNewEtcdPeerCertAndKey(t *testing.T) {
}
}
func TestNewEtcdHealthcheckClientCertAndKey(t *testing.T) {
caCert, caKey, err := NewCACertAndKey()
if err != nil {
t.Fatalf("failed creation of ca cert and key: %v", err)
}
etcdHealthcheckClientCert, _, err := NewEtcdHealthcheckClientCertAndKey(caCert, caKey)
if err != nil {
t.Fatalf("failed creation of cert and key: %v", err)
}
certstestutil.AssertCertificateIsSignedByCa(t, etcdHealthcheckClientCert, caCert)
certstestutil.AssertCertificateHasClientAuthUsage(t, etcdHealthcheckClientCert)
certstestutil.AssertCertificateHasOrganizations(t, etcdHealthcheckClientCert, kubeadmconstants.MastersGroup)
}
func TestNewAPIServerEtcdClientCertAndKey(t *testing.T) {
caCert, caKey, err := NewCACertAndKey()
if err != nil {
@ -595,6 +611,7 @@ func TestCreateCertificateFilesMethods(t *testing.T) {
kubeadmconstants.EtcdCACertName, kubeadmconstants.EtcdCAKeyName,
kubeadmconstants.EtcdServerCertName, kubeadmconstants.EtcdServerKeyName,
kubeadmconstants.EtcdPeerCertName, kubeadmconstants.EtcdPeerKeyName,
kubeadmconstants.EtcdHealthcheckClientCertName, kubeadmconstants.EtcdHealthcheckClientKeyName,
kubeadmconstants.APIServerEtcdClientCertName, kubeadmconstants.APIServerEtcdClientKeyName,
kubeadmconstants.ServiceAccountPrivateKeyName, kubeadmconstants.ServiceAccountPublicKeyName,
kubeadmconstants.FrontProxyCACertName, kubeadmconstants.FrontProxyCAKeyName,
@ -629,6 +646,11 @@ func TestCreateCertificateFilesMethods(t *testing.T) {
createFunc: CreateEtcdPeerCertAndKeyFiles,
expectedFiles: []string{kubeadmconstants.EtcdPeerCertName, kubeadmconstants.EtcdPeerKeyName},
},
{
setupFunc: CreateEtcdCACertAndKeyFiles,
createFunc: CreateEtcdHealthcheckClientCertAndKeyFiles,
expectedFiles: []string{kubeadmconstants.EtcdHealthcheckClientCertName, kubeadmconstants.EtcdHealthcheckClientKeyName},
},
{
setupFunc: CreateEtcdCACertAndKeyFiles,
createFunc: CreateAPIServerEtcdClientCertAndKeyFiles,

View File

@ -46,6 +46,8 @@ package certs
- etcd/server.key
- etcd/peer.crt
- etcd/peer.key
- etcd/healthcheck-client.crt
- etcd/healthcheck-client.key
- sa.pub
- sa.key
- front-proxy-ca.crt

View File

@ -65,7 +65,10 @@ func GetEtcdPodSpec(cfg *kubeadmapi.MasterConfiguration) v1.Pod {
staticpodutil.NewVolumeMount(etcdVolumeName, cfg.Etcd.DataDir, false),
staticpodutil.NewVolumeMount(certsVolumeName, cfg.CertificatesDir+"/etcd", false),
},
LivenessProbe: staticpodutil.ComponentProbe(cfg, kubeadmconstants.Etcd, 2379, "/health", v1.URISchemeHTTP),
LivenessProbe: staticpodutil.EtcdProbe(
cfg, kubeadmconstants.Etcd, 2379, cfg.CertificatesDir,
kubeadmconstants.EtcdCACertName, kubeadmconstants.EtcdHealthcheckClientCertName, kubeadmconstants.EtcdHealthcheckClientKeyName,
),
}, etcdMounts)
}

View File

@ -148,6 +148,9 @@ func upgradeComponent(component string, waiter apiclient.Waiter, pathMgr StaticP
if err := certsphase.CreateEtcdPeerCertAndKeyFiles(cfg); err != nil {
return fmt.Errorf("failed to upgrade the %s peer certificate and key: %v", constants.Etcd, err)
}
if err := certsphase.CreateEtcdHealthcheckClientCertAndKeyFiles(cfg); err != nil {
return fmt.Errorf("failed to upgrade the %s healthcheck certificate and key: %v", constants.Etcd, err)
}
}
if component == constants.KubeAPIServer {
if err := certsphase.CreateAPIServerEtcdClientCertAndKeyFiles(cfg); err != nil {

View File

@ -327,6 +327,7 @@ func TestStaticPodControlPlane(t *testing.T) {
// certsphase.CreateEtcdCACertAndKeyFiles,
// certsphase.CreateEtcdServerCertAndKeyFiles,
// certsphase.CreateEtcdPeerCertAndKeyFiles,
// certsphase.CreateEtcdHealthcheckClientCertAndKeyFiles,
// certsphase.CreateAPIServerEtcdClientCertAndKeyFiles,
certsphase.CreateServiceAccountKeyAndPublicKeyFiles,
certsphase.CreateFrontProxyCACertAndKeyFiles,

View File

@ -97,6 +97,24 @@ func ComponentProbe(cfg *kubeadmapi.MasterConfiguration, componentName string, p
}
}
// EtcdProbe is a helper function for building a shell-based, etcdctl v1.Probe object to healthcheck etcd
func EtcdProbe(cfg *kubeadmapi.MasterConfiguration, componentName string, port int, certsDir string, CACertName string, CertName string, KeyName string) *v1.Probe {
tlsFlags := fmt.Sprintf("--cacert=%[1]s/%[2]s --cert=%[1]s/%[3]s --key=%[1]s/%[4]s", certsDir, CACertName, CertName, KeyName)
// etcd pod is alive if a linearizable get succeeds.
cmd := fmt.Sprintf("ETCDCTL_API=3 etcdctl --endpoints=%s:%d %s get foo", GetProbeAddress(cfg, componentName), port, tlsFlags)
return &v1.Probe{
Handler: v1.Handler{
Exec: &v1.ExecAction{
Command: []string{"/bin/sh", "-ec", cmd},
},
},
InitialDelaySeconds: 15,
TimeoutSeconds: 15,
FailureThreshold: 8,
}
}
// NewVolume creates a v1.Volume with a hostPath mount to the specified location
func NewVolume(name, path string, pathType *v1.HostPathType) v1.Volume {
return v1.Volume{

View File

@ -161,48 +161,6 @@ func TestComponentProbe(t *testing.T) {
scheme: v1.URISchemeHTTP,
expected: "2001:db8::1",
},
{
name: "valid etcd probe using listen-client-urls IPv4 addresses",
cfg: &kubeadmapi.MasterConfiguration{
Etcd: kubeadmapi.Etcd{
ExtraArgs: map[string]string{
"listen-client-urls": "http://1.2.3.4:2379,http://4.3.2.1:2379"},
},
},
component: kubeadmconstants.Etcd,
port: 1,
path: "foo",
scheme: v1.URISchemeHTTP,
expected: "1.2.3.4",
},
{
name: "valid etcd probe using listen-client-urls IPv6 addresses",
cfg: &kubeadmapi.MasterConfiguration{
Etcd: kubeadmapi.Etcd{
ExtraArgs: map[string]string{
"listen-client-urls": "http://[2001:db8::1]:2379,http://[2001:db8::2]:2379"},
},
},
component: kubeadmconstants.Etcd,
port: 1,
path: "foo",
scheme: v1.URISchemeHTTP,
expected: "2001:db8::1",
},
{
name: "valid IPv4 etcd probe using hostname for listen-client-urls",
cfg: &kubeadmapi.MasterConfiguration{
Etcd: kubeadmapi.Etcd{
ExtraArgs: map[string]string{
"listen-client-urls": "http://localhost:2379"},
},
},
component: kubeadmconstants.Etcd,
port: 1,
path: "foo",
scheme: v1.URISchemeHTTP,
expected: "127.0.0.1",
},
}
for _, rt := range tests {
actual := ComponentProbe(rt.cfg, rt.component, rt.port, rt.path, rt.scheme)
@ -229,6 +187,77 @@ func TestComponentProbe(t *testing.T) {
}
}
func TestEtcdProbe(t *testing.T) {
var tests = []struct {
name string
cfg *kubeadmapi.MasterConfiguration
component string
port int
certsDir string
cacert string
cert string
key string
expected string
}{
{
name: "valid etcd probe using listen-client-urls IPv4 addresses",
cfg: &kubeadmapi.MasterConfiguration{
Etcd: kubeadmapi.Etcd{
ExtraArgs: map[string]string{
"listen-client-urls": "http://1.2.3.4:2379,http://4.3.2.1:2379"},
},
},
component: kubeadmconstants.Etcd,
port: 1,
certsDir: "secretsA",
cacert: "ca1",
cert: "cert1",
key: "key1",
expected: "ETCDCTL_API=3 etcdctl --endpoints=1.2.3.4:1 --cacert=secretsA/ca1 --cert=secretsA/cert1 --key=secretsA/key1 get foo",
},
{
name: "valid etcd probe using listen-client-urls IPv6 addresses",
cfg: &kubeadmapi.MasterConfiguration{
Etcd: kubeadmapi.Etcd{
ExtraArgs: map[string]string{
"listen-client-urls": "http://[2001:db8::1]:2379,http://[2001:db8::2]:2379"},
},
},
component: kubeadmconstants.Etcd,
port: 1,
certsDir: "secretsB",
cacert: "ca2",
cert: "cert2",
key: "key2",
expected: "ETCDCTL_API=3 etcdctl --endpoints=2001:db8::1:1 --cacert=secretsB/ca2 --cert=secretsB/cert2 --key=secretsB/key2 get foo",
},
{
name: "valid IPv4 etcd probe using hostname for listen-client-urls",
cfg: &kubeadmapi.MasterConfiguration{
Etcd: kubeadmapi.Etcd{
ExtraArgs: map[string]string{
"listen-client-urls": "http://localhost:2379"},
},
},
component: kubeadmconstants.Etcd,
port: 1,
certsDir: "secretsC",
cacert: "ca3",
cert: "cert3",
key: "key3",
expected: "ETCDCTL_API=3 etcdctl --endpoints=127.0.0.1:1 --cacert=secretsC/ca3 --cert=secretsC/cert3 --key=secretsC/key3 get foo",
},
}
for _, rt := range tests {
actual := EtcdProbe(rt.cfg, rt.component, rt.port, rt.certsDir, rt.cacert, rt.cert, rt.key)
if actual.Handler.Exec.Command[2] != rt.expected {
t.Errorf("%s test case failed:\n\texpected: %s\n\t actual: %s",
rt.name, rt.expected,
actual.Handler.Exec.Command[2])
}
}
}
func TestComponentPod(t *testing.T) {
var tests = []struct {
name string

View File

@ -25,6 +25,7 @@ docs/admin/kubeadm_alpha_phase_certs_apiserver-kubelet-client.md
docs/admin/kubeadm_alpha_phase_certs_apiserver.md
docs/admin/kubeadm_alpha_phase_certs_ca.md
docs/admin/kubeadm_alpha_phase_certs_etcd-ca.md
docs/admin/kubeadm_alpha_phase_certs_etcd-healthcheck-client.md
docs/admin/kubeadm_alpha_phase_certs_etcd-peer.md
docs/admin/kubeadm_alpha_phase_certs_etcd-server.md
docs/admin/kubeadm_alpha_phase_certs_front-proxy-ca.md
@ -92,6 +93,7 @@ docs/man/man1/kubeadm-alpha-phase-certs-apiserver-kubelet-client.1
docs/man/man1/kubeadm-alpha-phase-certs-apiserver.1
docs/man/man1/kubeadm-alpha-phase-certs-ca.1
docs/man/man1/kubeadm-alpha-phase-certs-etcd-ca.1
docs/man/man1/kubeadm-alpha-phase-certs-etcd-healthcheck-client.1
docs/man/man1/kubeadm-alpha-phase-certs-etcd-peer.1
docs/man/man1/kubeadm-alpha-phase-certs-etcd-server.1
docs/man/man1/kubeadm-alpha-phase-certs-front-proxy-ca.1

View File

@ -0,0 +1,3 @@
This file is autogenerated, but we've stopped checking such files into the
repository to reduce the need for rebases. Please run hack/generate-docs.sh to
populate this file.

View File

@ -0,0 +1,3 @@
This file is autogenerated, but we've stopped checking such files into the
repository to reduce the need for rebases. Please run hack/generate-docs.sh to
populate this file.