diff --git a/nodeup/pkg/model/context.go b/nodeup/pkg/model/context.go index ae5974244b04b..1ee774c087f90 100644 --- a/nodeup/pkg/model/context.go +++ b/nodeup/pkg/model/context.go @@ -116,7 +116,7 @@ func (c *NodeupModelContext) CNIConfDir() string { // buildPKIKubeconfig generates a kubeconfig func (c *NodeupModelContext) buildPKIKubeconfig(id string) (string, error) { - caCertificate, err := c.KeyStore.Cert(fi.CertificateId_CA, false) + caCertificate, err := c.KeyStore.FindCert(fi.CertificateId_CA) if err != nil { return "", fmt.Errorf("error fetching CA certificate from keystore: %v", err) } diff --git a/nodeup/pkg/model/kube_apiserver.go b/nodeup/pkg/model/kube_apiserver.go index 558626a0ec1fc..9ddbef420b9f2 100644 --- a/nodeup/pkg/model/kube_apiserver.go +++ b/nodeup/pkg/model/kube_apiserver.go @@ -184,12 +184,25 @@ func (b *KubeAPIServerBuilder) buildPod() (*v1.Pod, error) { } if b.IsKubernetesGTE("1.7") { - certPath := filepath.Join(b.PathSrvKubernetes(), "proxy-client.cert") + certPath := filepath.Join(b.PathSrvKubernetes(), "apiserver-aggregator.cert") kubeAPIServer.ProxyClientCertFile = &certPath - keyPath := filepath.Join(b.PathSrvKubernetes(), "proxy-client.key") + keyPath := filepath.Join(b.PathSrvKubernetes(), "apiserver-aggregator.key") kubeAPIServer.ProxyClientKeyFile = &keyPath } + // APIServer aggregation options + if b.IsKubernetesGTE("1.7") { + cert, err := b.KeyStore.FindCert("apiserver-aggregator-ca") + if err != nil { + return nil, fmt.Errorf("apiserver aggregator CA cert lookup failed: %v", err.Error()) + } + + if cert != nil { + certPath := filepath.Join(b.PathSrvKubernetes(), "apiserver-aggregator-ca.cert") + kubeAPIServer.RequestheaderClientCAFile = certPath + } + } + // build the kube-apiserver flags for the service flags, err := flagbuilder.BuildFlagsList(b.Cluster.Spec.KubeAPIServer) if err != nil { diff --git a/nodeup/pkg/model/secrets.go b/nodeup/pkg/model/secrets.go index bcb2147d77d4d..b7bd4003d71e7 100644 --- a/nodeup/pkg/model/secrets.go +++ b/nodeup/pkg/model/secrets.go @@ -21,6 +21,7 @@ import ( "path/filepath" "strings" + "github.com/golang/glog" "k8s.io/kops/upup/pkg/fi" "k8s.io/kops/upup/pkg/fi/nodeup/nodetasks" ) @@ -117,7 +118,7 @@ func (b *SecretBuilder) Build(c *fi.ModelBuilderContext) error { } if b.IsKubernetesGTE("1.7") { - + // TODO: Remove - we use the apiserver-aggregator keypair instead (which is signed by a different CA) cert, err := b.KeyStore.Cert("apiserver-proxy-client", false) if err != nil { return fmt.Errorf("apiserver proxy client cert lookup failed: %v", err.Error()) @@ -153,6 +154,22 @@ func (b *SecretBuilder) Build(c *fi.ModelBuilderContext) error { c.AddTask(t) } + if b.IsKubernetesGTE("1.7") { + if err := b.writeCertificate(c, "apiserver-aggregator"); err != nil { + return err + } + + if err := b.writePrivateKey(c, "apiserver-aggregator"); err != nil { + return err + } + } + + if b.IsKubernetesGTE("1.7") { + if err := b.writeCertificate(c, "apiserver-aggregator-ca"); err != nil { + return err + } + } + if b.SecretStore != nil { key := "kube" token, err := b.SecretStore.FindSecret(key) @@ -200,6 +217,55 @@ func (b *SecretBuilder) Build(c *fi.ModelBuilderContext) error { return nil } +// writeCertificate writes the specified certificate to the local filesystem, under PathSrvKubernetes() +func (b *SecretBuilder) writeCertificate(c *fi.ModelBuilderContext, id string) error { + cert, err := b.KeyStore.FindCert(id) + if err != nil { + return fmt.Errorf("cert lookup failed for %q: %v", id, err) + } + + if cert != nil { + serialized, err := cert.AsString() + if err != nil { + return err + } + + t := &nodetasks.File{ + Path: filepath.Join(b.PathSrvKubernetes(), id+".cert"), + Contents: fi.NewStringResource(serialized), + Type: nodetasks.FileType_File, + } + c.AddTask(t) + } else { + // TODO: Make this an error? + glog.Warningf("certificate %q not found", id) + } + + return nil +} + +// writePrivateKey writes the specified private key to the local filesystem, under PathSrvKubernetes() +func (b *SecretBuilder) writePrivateKey(c *fi.ModelBuilderContext, id string) error { + key, err := b.KeyStore.FindPrivateKey(id) + if err != nil { + return fmt.Errorf("private key lookup failed for %q: %v", id, err) + } + + serialized, err := key.AsString() + if err != nil { + return err + } + + t := &nodetasks.File{ + Path: filepath.Join(b.PathSrvKubernetes(), id+".key"), + Contents: fi.NewStringResource(serialized), + Type: nodetasks.FileType_File, + } + c.AddTask(t) + + return nil +} + // allTokens returns a map of all tokens func (b *SecretBuilder) allTokens() (map[string]string, error) { tokens := make(map[string]string) diff --git a/pkg/apis/kops/componentconfig.go b/pkg/apis/kops/componentconfig.go index c4baab07ac28b..e10484fe4cea0 100644 --- a/pkg/apis/kops/componentconfig.go +++ b/pkg/apis/kops/componentconfig.go @@ -258,6 +258,17 @@ type KubeAPIServerConfig struct { AuthorizationRBACSuperUser *string `json:"authorizationRbacSuperUser,omitempty" flag:"authorization-rbac-super-user"` // ExperimentalEncryptionProviderConfig enables encryption at rest for secrets. ExperimentalEncryptionProviderConfig *string `json:"experimentalEncryptionProviderConfig,omitempty" flag:"experimental-encryption-provider-config"` + + // List of request headers to inspect for usernames. X-Remote-User is common. + RequestheaderUsernameHeaders []string `json:"requestheaderUsernameHeaders,omitempty" flag:"requestheader-username-headers"` + // List of request headers to inspect for groups. X-Remote-Group is suggested. + RequestheaderGroupHeaders []string `json:"requestheaderGroupHeaders,omitempty" flag:"requestheader-group-headers"` + // List of request header prefixes to inspect. X-Remote-Extra- is suggested. + RequestheaderExtraHeaderPrefixes []string `json:"requestheaderExtraHeaderPrefixes,omitempty" flag:"requestheader-extra-headers-prefix"` + //Root certificate bundle to use to verify client certificates on incoming requests before trusting usernames in headers specified by --requestheader-username-headers + RequestheaderClientCAFile string `json:"requestheaderClientCAFile,omitempty" flag:"requestheader-client-ca-file"` + // List of client certificate common names to allow to provide usernames in headers specified by --requestheader-username-headers. If empty, any client certificate validated by the authorities in --requestheader-client-ca-file is allowed. + RequestheaderAllowedNames []string `json:"requestheaderAllowedNames,omitempty" flag:"requestheader-allowed-names"` } // KubeControllerManagerConfig is the configuration for the controller diff --git a/pkg/apis/kops/v1alpha1/componentconfig.go b/pkg/apis/kops/v1alpha1/componentconfig.go index 6882ba357f751..bc746b301294d 100644 --- a/pkg/apis/kops/v1alpha1/componentconfig.go +++ b/pkg/apis/kops/v1alpha1/componentconfig.go @@ -258,6 +258,17 @@ type KubeAPIServerConfig struct { AuthorizationRBACSuperUser *string `json:"authorizationRbacSuperUser,omitempty" flag:"authorization-rbac-super-user"` // ExperimentalEncryptionProviderConfig enables encryption at rest for secrets. ExperimentalEncryptionProviderConfig *string `json:"experimentalEncryptionProviderConfig,omitempty" flag:"experimental-encryption-provider-config"` + + // List of request headers to inspect for usernames. X-Remote-User is common. + RequestheaderUsernameHeaders []string `json:"requestheaderUsernameHeaders,omitempty" flag:"requestheader-username-headers"` + // List of request headers to inspect for groups. X-Remote-Group is suggested. + RequestheaderGroupHeaders []string `json:"requestheaderGroupHeaders,omitempty" flag:"requestheader-group-headers"` + // List of request header prefixes to inspect. X-Remote-Extra- is suggested. + RequestheaderExtraHeaderPrefixes []string `json:"requestheaderExtraHeaderPrefixes,omitempty" flag:"requestheader-extra-headers-prefix"` + //Root certificate bundle to use to verify client certificates on incoming requests before trusting usernames in headers specified by --requestheader-username-headers + RequestheaderClientCAFile string `json:"requestheaderClientCAFile,omitempty" flag:"requestheader-client-ca-file"` + // List of client certificate common names to allow to provide usernames in headers specified by --requestheader-username-headers. If empty, any client certificate validated by the authorities in --requestheader-client-ca-file is allowed. + RequestheaderAllowedNames []string `json:"requestheaderAllowedNames,omitempty" flag:"requestheader-allowed-names"` } // KubeControllerManagerConfig is the configuration for the controller diff --git a/pkg/apis/kops/v1alpha1/zz_generated.conversion.go b/pkg/apis/kops/v1alpha1/zz_generated.conversion.go index 4c464d15a36b6..cabb254db933b 100644 --- a/pkg/apis/kops/v1alpha1/zz_generated.conversion.go +++ b/pkg/apis/kops/v1alpha1/zz_generated.conversion.go @@ -1810,6 +1810,11 @@ func autoConvert_v1alpha1_KubeAPIServerConfig_To_kops_KubeAPIServerConfig(in *Ku out.AuthorizationMode = in.AuthorizationMode out.AuthorizationRBACSuperUser = in.AuthorizationRBACSuperUser out.ExperimentalEncryptionProviderConfig = in.ExperimentalEncryptionProviderConfig + out.RequestheaderUsernameHeaders = in.RequestheaderUsernameHeaders + out.RequestheaderGroupHeaders = in.RequestheaderGroupHeaders + out.RequestheaderExtraHeaderPrefixes = in.RequestheaderExtraHeaderPrefixes + out.RequestheaderClientCAFile = in.RequestheaderClientCAFile + out.RequestheaderAllowedNames = in.RequestheaderAllowedNames return nil } @@ -1862,6 +1867,11 @@ func autoConvert_kops_KubeAPIServerConfig_To_v1alpha1_KubeAPIServerConfig(in *ko out.AuthorizationMode = in.AuthorizationMode out.AuthorizationRBACSuperUser = in.AuthorizationRBACSuperUser out.ExperimentalEncryptionProviderConfig = in.ExperimentalEncryptionProviderConfig + out.RequestheaderUsernameHeaders = in.RequestheaderUsernameHeaders + out.RequestheaderGroupHeaders = in.RequestheaderGroupHeaders + out.RequestheaderExtraHeaderPrefixes = in.RequestheaderExtraHeaderPrefixes + out.RequestheaderClientCAFile = in.RequestheaderClientCAFile + out.RequestheaderAllowedNames = in.RequestheaderAllowedNames return nil } diff --git a/pkg/apis/kops/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/kops/v1alpha1/zz_generated.deepcopy.go index 917fcaeaa9f13..50dbb753b47f6 100644 --- a/pkg/apis/kops/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/kops/v1alpha1/zz_generated.deepcopy.go @@ -2017,6 +2017,26 @@ func (in *KubeAPIServerConfig) DeepCopyInto(out *KubeAPIServerConfig) { **out = **in } } + if in.RequestheaderUsernameHeaders != nil { + in, out := &in.RequestheaderUsernameHeaders, &out.RequestheaderUsernameHeaders + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.RequestheaderGroupHeaders != nil { + in, out := &in.RequestheaderGroupHeaders, &out.RequestheaderGroupHeaders + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.RequestheaderExtraHeaderPrefixes != nil { + in, out := &in.RequestheaderExtraHeaderPrefixes, &out.RequestheaderExtraHeaderPrefixes + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.RequestheaderAllowedNames != nil { + in, out := &in.RequestheaderAllowedNames, &out.RequestheaderAllowedNames + *out = make([]string, len(*in)) + copy(*out, *in) + } return } diff --git a/pkg/apis/kops/v1alpha2/componentconfig.go b/pkg/apis/kops/v1alpha2/componentconfig.go index d8701b56d6612..55ad405f2a214 100644 --- a/pkg/apis/kops/v1alpha2/componentconfig.go +++ b/pkg/apis/kops/v1alpha2/componentconfig.go @@ -258,6 +258,17 @@ type KubeAPIServerConfig struct { AuthorizationRBACSuperUser *string `json:"authorizationRbacSuperUser,omitempty" flag:"authorization-rbac-super-user"` // ExperimentalEncryptionProviderConfig enables encryption at rest for secrets. ExperimentalEncryptionProviderConfig *string `json:"experimentalEncryptionProviderConfig,omitempty" flag:"experimental-encryption-provider-config"` + + // List of request headers to inspect for usernames. X-Remote-User is common. + RequestheaderUsernameHeaders []string `json:"requestheaderUsernameHeaders,omitempty" flag:"requestheader-username-headers"` + // List of request headers to inspect for groups. X-Remote-Group is suggested. + RequestheaderGroupHeaders []string `json:"requestheaderGroupHeaders,omitempty" flag:"requestheader-group-headers"` + // List of request header prefixes to inspect. X-Remote-Extra- is suggested. + RequestheaderExtraHeaderPrefixes []string `json:"requestheaderExtraHeaderPrefixes,omitempty" flag:"requestheader-extra-headers-prefix"` + //Root certificate bundle to use to verify client certificates on incoming requests before trusting usernames in headers specified by --requestheader-username-headers + RequestheaderClientCAFile string `json:"requestheaderClientCAFile,omitempty" flag:"requestheader-client-ca-file"` + // List of client certificate common names to allow to provide usernames in headers specified by --requestheader-username-headers. If empty, any client certificate validated by the authorities in --requestheader-client-ca-file is allowed. + RequestheaderAllowedNames []string `json:"requestheaderAllowedNames,omitempty" flag:"requestheader-allowed-names"` } // KubeControllerManagerConfig is the configuration for the controller diff --git a/pkg/apis/kops/v1alpha2/zz_generated.conversion.go b/pkg/apis/kops/v1alpha2/zz_generated.conversion.go index 4c1046fa01b28..725bada90a1e2 100644 --- a/pkg/apis/kops/v1alpha2/zz_generated.conversion.go +++ b/pkg/apis/kops/v1alpha2/zz_generated.conversion.go @@ -2072,6 +2072,11 @@ func autoConvert_v1alpha2_KubeAPIServerConfig_To_kops_KubeAPIServerConfig(in *Ku out.AuthorizationMode = in.AuthorizationMode out.AuthorizationRBACSuperUser = in.AuthorizationRBACSuperUser out.ExperimentalEncryptionProviderConfig = in.ExperimentalEncryptionProviderConfig + out.RequestheaderUsernameHeaders = in.RequestheaderUsernameHeaders + out.RequestheaderGroupHeaders = in.RequestheaderGroupHeaders + out.RequestheaderExtraHeaderPrefixes = in.RequestheaderExtraHeaderPrefixes + out.RequestheaderClientCAFile = in.RequestheaderClientCAFile + out.RequestheaderAllowedNames = in.RequestheaderAllowedNames return nil } @@ -2124,6 +2129,11 @@ func autoConvert_kops_KubeAPIServerConfig_To_v1alpha2_KubeAPIServerConfig(in *ko out.AuthorizationMode = in.AuthorizationMode out.AuthorizationRBACSuperUser = in.AuthorizationRBACSuperUser out.ExperimentalEncryptionProviderConfig = in.ExperimentalEncryptionProviderConfig + out.RequestheaderUsernameHeaders = in.RequestheaderUsernameHeaders + out.RequestheaderGroupHeaders = in.RequestheaderGroupHeaders + out.RequestheaderExtraHeaderPrefixes = in.RequestheaderExtraHeaderPrefixes + out.RequestheaderClientCAFile = in.RequestheaderClientCAFile + out.RequestheaderAllowedNames = in.RequestheaderAllowedNames return nil } diff --git a/pkg/apis/kops/v1alpha2/zz_generated.deepcopy.go b/pkg/apis/kops/v1alpha2/zz_generated.deepcopy.go index 0ece8f2c362d7..2e4c17677a359 100644 --- a/pkg/apis/kops/v1alpha2/zz_generated.deepcopy.go +++ b/pkg/apis/kops/v1alpha2/zz_generated.deepcopy.go @@ -2143,6 +2143,26 @@ func (in *KubeAPIServerConfig) DeepCopyInto(out *KubeAPIServerConfig) { **out = **in } } + if in.RequestheaderUsernameHeaders != nil { + in, out := &in.RequestheaderUsernameHeaders, &out.RequestheaderUsernameHeaders + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.RequestheaderGroupHeaders != nil { + in, out := &in.RequestheaderGroupHeaders, &out.RequestheaderGroupHeaders + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.RequestheaderExtraHeaderPrefixes != nil { + in, out := &in.RequestheaderExtraHeaderPrefixes, &out.RequestheaderExtraHeaderPrefixes + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.RequestheaderAllowedNames != nil { + in, out := &in.RequestheaderAllowedNames, &out.RequestheaderAllowedNames + *out = make([]string, len(*in)) + copy(*out, *in) + } return } diff --git a/pkg/apis/kops/zz_generated.deepcopy.go b/pkg/apis/kops/zz_generated.deepcopy.go index 07d3d35052b58..b825e1e516813 100644 --- a/pkg/apis/kops/zz_generated.deepcopy.go +++ b/pkg/apis/kops/zz_generated.deepcopy.go @@ -2362,6 +2362,26 @@ func (in *KubeAPIServerConfig) DeepCopyInto(out *KubeAPIServerConfig) { **out = **in } } + if in.RequestheaderUsernameHeaders != nil { + in, out := &in.RequestheaderUsernameHeaders, &out.RequestheaderUsernameHeaders + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.RequestheaderGroupHeaders != nil { + in, out := &in.RequestheaderGroupHeaders, &out.RequestheaderGroupHeaders + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.RequestheaderExtraHeaderPrefixes != nil { + in, out := &in.RequestheaderExtraHeaderPrefixes, &out.RequestheaderExtraHeaderPrefixes + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.RequestheaderAllowedNames != nil { + in, out := &in.RequestheaderAllowedNames, &out.RequestheaderAllowedNames + *out = make([]string, len(*in)) + copy(*out, *in) + } return } diff --git a/pkg/model/components/apiserver.go b/pkg/model/components/apiserver.go index c515449c7f302..2d0d6c8bac306 100644 --- a/pkg/model/components/apiserver.go +++ b/pkg/model/components/apiserver.go @@ -94,6 +94,10 @@ func (b *KubeAPIServerOptionsBuilder) BuildOptions(o interface{}) error { clusterSpec.KubeAPIServer.AuthorizationMode = fi.String("RBAC") } + if err := b.configureAggregation(clusterSpec); err != nil { + return nil + } + image, err := Image("kube-apiserver", clusterSpec, b.AssetBuilder) if err != nil { return err @@ -243,3 +247,15 @@ func (b *KubeAPIServerOptionsBuilder) buildAPIServerCount(clusterSpec *kops.Clus return count } + +// configureAggregation sets up the aggregation options +func (b *KubeAPIServerOptionsBuilder) configureAggregation(clusterSpec *kops.ClusterSpec) error { + if b.IsKubernetesGTE("1.7") { + clusterSpec.KubeAPIServer.RequestheaderAllowedNames = []string{"aggregator"} + clusterSpec.KubeAPIServer.RequestheaderExtraHeaderPrefixes = []string{"X-Remote-Extra-"} + clusterSpec.KubeAPIServer.RequestheaderGroupHeaders = []string{"X-Remote-Group"} + clusterSpec.KubeAPIServer.RequestheaderUsernameHeaders = []string{"X-Remote-User"} + } + + return nil +} diff --git a/pkg/model/pki.go b/pkg/model/pki.go index 219cad2a5bd5f..059824a75d6c7 100644 --- a/pkg/model/pki.go +++ b/pkg/model/pki.go @@ -35,13 +35,25 @@ var _ fi.ModelBuilder = &PKIModelBuilder{} // Build is responsible for generating the various pki assets func (b *PKIModelBuilder) Build(c *fi.ModelBuilderContext) error { + + // TODO: Only create the CA via this task + defaultCA := &fitasks.Keypair{ + Name: fi.String(fi.CertificateId_CA), + Lifecycle: b.Lifecycle, + Subject: "cn=kubernetes", + Type: "ca", + } + c.AddTask(defaultCA) + { + t := &fitasks.Keypair{ Name: fi.String("kubelet"), Lifecycle: b.Lifecycle, Subject: "o=" + user.NodesGroup + ",cn=kubelet", Type: "client", + Signer: defaultCA, } c.AddTask(t) } @@ -54,6 +66,7 @@ func (b *PKIModelBuilder) Build(c *fi.ModelBuilderContext) error { Lifecycle: b.Lifecycle, Subject: "cn=kubelet-api", Type: "client", + Signer: defaultCA, }) } { @@ -62,6 +75,7 @@ func (b *PKIModelBuilder) Build(c *fi.ModelBuilderContext) error { Lifecycle: b.Lifecycle, Subject: "cn=" + user.KubeScheduler, Type: "client", + Signer: defaultCA, } c.AddTask(t) } @@ -72,6 +86,7 @@ func (b *PKIModelBuilder) Build(c *fi.ModelBuilderContext) error { Lifecycle: b.Lifecycle, Subject: "cn=" + user.KubeProxy, Type: "client", + Signer: defaultCA, } c.AddTask(t) } @@ -82,6 +97,7 @@ func (b *PKIModelBuilder) Build(c *fi.ModelBuilderContext) error { Lifecycle: b.Lifecycle, Subject: "cn=" + user.KubeControllerManager, Type: "client", + Signer: defaultCA, } c.AddTask(t) } @@ -101,6 +117,7 @@ func (b *PKIModelBuilder) Build(c *fi.ModelBuilderContext) error { Name: fi.String("etcd"), Subject: "cn=etcd", Type: "server", + Signer: defaultCA, }) } { @@ -109,6 +126,7 @@ func (b *PKIModelBuilder) Build(c *fi.ModelBuilderContext) error { Lifecycle: b.Lifecycle, Subject: "cn=etcd-client", Type: "client", + Signer: defaultCA, }) } } @@ -118,6 +136,7 @@ func (b *PKIModelBuilder) Build(c *fi.ModelBuilderContext) error { Name: fi.String("kube-router"), Subject: "cn=" + "system:kube-router", Type: "client", + Signer: defaultCA, } c.AddTask(t) } @@ -128,6 +147,7 @@ func (b *PKIModelBuilder) Build(c *fi.ModelBuilderContext) error { Lifecycle: b.Lifecycle, Subject: "o=" + user.SystemPrivilegedGroup + ",cn=kubecfg", Type: "client", + Signer: defaultCA, } c.AddTask(t) } @@ -138,16 +158,38 @@ func (b *PKIModelBuilder) Build(c *fi.ModelBuilderContext) error { Lifecycle: b.Lifecycle, Subject: "cn=apiserver-proxy-client", Type: "client", + Signer: defaultCA, } c.AddTask(t) } + { + aggregatorCA := &fitasks.Keypair{ + Name: fi.String("apiserver-aggregator-ca"), + Lifecycle: b.Lifecycle, + Subject: "cn=apiserver-aggregator-ca", + Type: "ca", + } + c.AddTask(aggregatorCA) + + aggregator := &fitasks.Keypair{ + Name: fi.String("apiserver-aggregator"), + Lifecycle: b.Lifecycle, + // Must match RequestheaderAllowedNames + Subject: "cn=aggregator", + Type: "client", + Signer: aggregatorCA, + } + c.AddTask(aggregator) + } + { t := &fitasks.Keypair{ Name: fi.String("kops"), Lifecycle: b.Lifecycle, Subject: "o=" + user.SystemPrivilegedGroup + ",cn=kops", Type: "client", + Signer: defaultCA, } c.AddTask(t) } @@ -183,6 +225,7 @@ func (b *PKIModelBuilder) Build(c *fi.ModelBuilderContext) error { Subject: "cn=kubernetes-master", Type: "server", AlternateNames: alternateNames, + Signer: defaultCA, } c.AddTask(t) } diff --git a/pkg/pki/csr.go b/pkg/pki/csr.go index e82187ccd6e58..b3a8f3027a7d2 100644 --- a/pkg/pki/csr.go +++ b/pkg/pki/csr.go @@ -85,7 +85,7 @@ func SignNewCertificate(privateKey *PrivateKey, template *x509.Certificate, sign template.KeyUsage = x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment } - if template.ExtKeyUsage == nil { + if template.ExtKeyUsage == nil && !template.IsCA { template.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth} } //c.SignatureAlgorithm = do we want to overrride? diff --git a/upup/pkg/fi/ca.go b/upup/pkg/fi/ca.go index c23b504e8ca7a..c0ea86907e813 100644 --- a/upup/pkg/fi/ca.go +++ b/upup/pkg/fi/ca.go @@ -49,7 +49,7 @@ type Keystore interface { // (if the certificate is found but not keypair, that is not an error: only the cert will be returned) FindKeypair(name string) (*pki.Certificate, *pki.PrivateKey, error) - CreateKeypair(name string, template *x509.Certificate, privateKey *pki.PrivateKey) (*pki.Certificate, error) + CreateKeypair(signer string, name string, template *x509.Certificate, privateKey *pki.PrivateKey) (*pki.Certificate, error) // StoreKeypair writes the keypair to the store StoreKeypair(id string, cert *pki.Certificate, privateKey *pki.PrivateKey) error @@ -67,11 +67,13 @@ type CAStore interface { Keystore // Cert returns the primary specified certificate + // For createIfMissing=false, using FindCert is preferred Cert(name string, createIfMissing bool) (*pki.Certificate, error) // CertificatePool returns all active certificates with the specified id CertificatePool(name string, createIfMissing bool) (*CertificatePool, error) PrivateKey(name string, createIfMissing bool) (*pki.PrivateKey, error) + // FindCert returns the specified certificate, if it exists, or nil if not found FindCert(name string) (*pki.Certificate, error) FindPrivateKey(name string) (*pki.PrivateKey, error) diff --git a/upup/pkg/fi/clientset_castore.go b/upup/pkg/fi/clientset_castore.go index 01c46477217ab..eb807cc07ac9e 100644 --- a/upup/pkg/fi/clientset_castore.go +++ b/upup/pkg/fi/clientset_castore.go @@ -42,8 +42,8 @@ type ClientsetCAStore struct { namespace string clientset kopsinternalversion.KopsInterface - mutex sync.Mutex - cacheCaKeyset *keyset + mutex sync.Mutex + cachedCaKeysets map[string]*keyset } var _ CAStore = &ClientsetCAStore{} @@ -51,42 +51,44 @@ var _ CAStore = &ClientsetCAStore{} // NewClientsetCAStore is the constructor for ClientsetCAStore func NewClientsetCAStore(clientset kopsinternalversion.KopsInterface, namespace string) CAStore { c := &ClientsetCAStore{ - clientset: clientset, - namespace: namespace, + clientset: clientset, + namespace: namespace, + cachedCaKeysets: make(map[string]*keyset), } return c } // readCAKeypairs retrieves the CA keypair, generating a new keypair if not found -func (c *ClientsetCAStore) readCAKeypairs() (*keyset, error) { +func (c *ClientsetCAStore) readCAKeypairs(id string) (*keyset, error) { c.mutex.Lock() defer c.mutex.Unlock() - if c.cacheCaKeyset != nil { - return c.cacheCaKeyset, nil + cached := c.cachedCaKeysets[id] + if cached != nil { + return cached, nil } - keyset, err := c.loadKeyset(CertificateId_CA) + keyset, err := c.loadKeyset(id) if err != nil { return nil, err } if keyset == nil { - keyset, err = c.generateCACertificate() + keyset, err = c.generateCACertificate(id) if err != nil { return nil, err } } - c.cacheCaKeyset = keyset + c.cachedCaKeysets[id] = keyset return keyset, nil } // generateCACertificate creates and stores a CA keypair // Should be called with the mutex held, to prevent concurrent creation of different keys -func (c *ClientsetCAStore) generateCACertificate() (*keyset, error) { +func (c *ClientsetCAStore) generateCACertificate(id string) (*keyset, error) { template := BuildCAX509Template() caRsaKey, err := rsa.GenerateKey(crypto_rand.Reader, 2048) @@ -104,7 +106,7 @@ func (c *ClientsetCAStore) generateCACertificate() (*keyset, error) { return nil, err } - return c.storeAndVerifyKeypair(CertificateId_CA, caCertificate, caPrivateKey) + return c.storeAndVerifyKeypair(id, caCertificate, caPrivateKey) } // keyset is a parsed Keyset @@ -310,12 +312,12 @@ func (c *ClientsetCAStore) List() ([]*KeystoreItem, error) { } // IssueCert implements CAStore::IssueCert -func (c *ClientsetCAStore) IssueCert(name string, serial *big.Int, privateKey *pki.PrivateKey, template *x509.Certificate) (*pki.Certificate, error) { +func (c *ClientsetCAStore) IssueCert(signer string, name string, serial *big.Int, privateKey *pki.PrivateKey, template *x509.Certificate) (*pki.Certificate, error) { glog.Infof("Issuing new certificate: %q", name) template.SerialNumber = serial - caKeyset, err := c.readCAKeypairs() + caKeyset, err := c.readCAKeypairs(signer) if err != nil { return nil, err } @@ -416,10 +418,10 @@ func (c *ClientsetCAStore) PrivateKey(name string, createIfMissing bool) (*pki.P } // CreateKeypair implements CAStore::CreateKeypair -func (c *ClientsetCAStore) CreateKeypair(id string, template *x509.Certificate, privateKey *pki.PrivateKey) (*pki.Certificate, error) { +func (c *ClientsetCAStore) CreateKeypair(signer string, id string, template *x509.Certificate, privateKey *pki.PrivateKey) (*pki.Certificate, error) { serial := c.buildSerial() - cert, err := c.IssueCert(id, serial, privateKey, template) + cert, err := c.IssueCert(signer, id, serial, privateKey, template) if err != nil { return nil, err } diff --git a/upup/pkg/fi/fitasks/keypair.go b/upup/pkg/fi/fitasks/keypair.go index 8054a3f13db8d..582c5924b8cdb 100644 --- a/upup/pkg/fi/fitasks/keypair.go +++ b/upup/pkg/fi/fitasks/keypair.go @@ -31,6 +31,7 @@ import ( var wellKnownCertificateTypes = map[string]string{ "client": "ExtKeyUsageClientAuth,KeyUsageDigitalSignature", "server": "ExtKeyUsageServerAuth,KeyUsageDigitalSignature,KeyUsageKeyEncipherment", + "ca": "CA,KeyUsageCRLSign,KeyUsageCertSign", } //go:generate fitask -type=Keypair @@ -41,6 +42,9 @@ type Keypair struct { Type string `json:"type"` AlternateNames []string `json:"alternateNames"` AlternateNameTasks []fi.Task `json:"alternateNameTasks"` + + // Signer is the keypair to use to sign, for when we want to use an alternative CA + Signer *Keypair } var _ fi.HasCheckExisting = &Keypair{} @@ -51,6 +55,12 @@ func (e *Keypair) CheckExisting(c *fi.Context) bool { return true } +var _ fi.CompareWithID = &Keypair{} + +func (e *Keypair) CompareWithID() *string { + return &e.Subject +} + func (e *Keypair) Find(c *fi.Context) (*Keypair, error) { name := fi.StringValue(e.Name) if name == "" { @@ -84,6 +94,8 @@ func (e *Keypair) Find(c *fi.Context) (*Keypair, error) { Type: buildTypeDescription(cert.Certificate), } + actual.Signer = &Keypair{Subject: pkixNameToString(&cert.Certificate.Issuer)} + // Avoid spurious changes actual.Lifecycle = e.Lifecycle @@ -133,7 +145,7 @@ func (e *Keypair) normalize(c *fi.Context) error { return nil } -func (s *Keypair) CheckChanges(a, e, changes *Keypair) error { +func (_ *Keypair) CheckChanges(a, e, changes *Keypair) error { if a != nil { if changes.Name != nil { return fi.CannotChangeField("Name") @@ -184,7 +196,11 @@ func (_ *Keypair) Render(c *fi.Context, a, e, changes *Keypair) error { } } - cert, err = c.Keystore.CreateKeypair(name, template, privateKey) + signer := fi.CertificateId_CA + if e.Signer != nil { + signer = fi.StringValue(e.Signer.Name) + } + cert, err = c.Keystore.CreateKeypair(signer, name, template, privateKey) if err != nil { return err } @@ -256,8 +272,10 @@ func buildCertificateTemplateForType(certificateType string) (*x509.Certificate, return nil, fmt.Errorf("unrecognized certificate option: %v", t) } template.ExtKeyUsage = append(template.ExtKeyUsage, ku) + } else if t == "CA" { + template.IsCA = true } else { - return nil, fmt.Errorf("unrecognized certificate option: %v", t) + return nil, fmt.Errorf("unrecognized certificate option: %q", t) } } diff --git a/upup/pkg/fi/k8sapi/k8s_keystore.go b/upup/pkg/fi/k8sapi/k8s_keystore.go index f650a45a3827c..26947005ebe3e 100644 --- a/upup/pkg/fi/k8sapi/k8s_keystore.go +++ b/upup/pkg/fi/k8sapi/k8s_keystore.go @@ -51,12 +51,12 @@ func NewKubernetesKeystore(client kubernetes.Interface, namespace string) fi.Key return c } -func (c *KubernetesKeystore) issueCert(id string, serial *big.Int, privateKey *pki.PrivateKey, template *x509.Certificate) (*pki.Certificate, error) { +func (c *KubernetesKeystore) issueCert(signer string, id string, serial *big.Int, privateKey *pki.PrivateKey, template *x509.Certificate) (*pki.Certificate, error) { glog.Infof("Issuing new certificate: %q", id) template.SerialNumber = serial - caCert, caKey, err := c.FindKeypair(fi.CertificateId_CA) + caCert, caKey, err := c.FindKeypair(signer) if err != nil { return nil, err } @@ -107,11 +107,11 @@ func (c *KubernetesKeystore) FindKeypair(id string) (*pki.Certificate, *pki.Priv return keypair.Certificate, keypair.PrivateKey, nil } -func (c *KubernetesKeystore) CreateKeypair(id string, template *x509.Certificate, privateKey *pki.PrivateKey) (*pki.Certificate, error) { +func (c *KubernetesKeystore) CreateKeypair(signer string, id string, template *x509.Certificate, privateKey *pki.PrivateKey) (*pki.Certificate, error) { t := time.Now().UnixNano() serial := pki.BuildPKISerial(t) - cert, err := c.issueCert(id, serial, privateKey, template) + cert, err := c.issueCert(signer, id, serial, privateKey, template) if err != nil { return nil, err } diff --git a/upup/pkg/fi/vfs_castore.go b/upup/pkg/fi/vfs_castore.go index e6557c34eb4bc..fb55be015d0d3 100644 --- a/upup/pkg/fi/vfs_castore.go +++ b/upup/pkg/fi/vfs_castore.go @@ -40,16 +40,21 @@ import ( type VFSCAStore struct { basedir vfs.Path - mutex sync.Mutex - cacheCaCertificates *certificates - cacheCaPrivateKeys *privateKeys + mutex sync.Mutex + cachedCAs map[string]*cachedEntry +} + +type cachedEntry struct { + certificates *certificates + privateKeys *privateKeys } var _ CAStore = &VFSCAStore{} func NewVFSCAStore(basedir vfs.Path) CAStore { c := &VFSCAStore{ - basedir: basedir, + basedir: basedir, + cachedCAs: make(map[string]*cachedEntry), } return c @@ -60,15 +65,16 @@ func (s *VFSCAStore) VFSPath() vfs.Path { } // Retrieves the CA keypair, generating a new keypair if not found -func (s *VFSCAStore) readCAKeypairs() (*certificates, *privateKeys, error) { +func (s *VFSCAStore) readCAKeypairs(id string) (*certificates, *privateKeys, error) { s.mutex.Lock() defer s.mutex.Unlock() - if s.cacheCaPrivateKeys != nil { - return s.cacheCaCertificates, s.cacheCaPrivateKeys, nil + cached := s.cachedCAs[id] + if cached != nil { + return cached.certificates, cached.privateKeys, nil } - caCertificates, err := s.loadCertificates(s.buildCertificatePoolPath(CertificateId_CA)) + caCertificates, err := s.loadCertificates(s.buildCertificatePoolPath(id)) if err != nil { return nil, nil, err } @@ -76,7 +82,7 @@ func (s *VFSCAStore) readCAKeypairs() (*certificates, *privateKeys, error) { var caPrivateKeys *privateKeys if caCertificates != nil { - caPrivateKeys, err = s.loadPrivateKeys(s.buildPrivateKeyPoolPath(CertificateId_CA)) + caPrivateKeys, err = s.loadPrivateKeys(s.buildPrivateKeyPoolPath(id)) if err != nil { return nil, nil, err } @@ -88,16 +94,16 @@ func (s *VFSCAStore) readCAKeypairs() (*certificates, *privateKeys, error) { } if caPrivateKeys == nil { - caCertificates, caPrivateKeys, err = s.generateCACertificate() + caCertificates, caPrivateKeys, err = s.generateCACertificate(id) if err != nil { return nil, nil, err } } - s.cacheCaCertificates = caCertificates - s.cacheCaPrivateKeys = caPrivateKeys + cached = &cachedEntry{certificates: caCertificates, privateKeys: caPrivateKeys} + s.cachedCAs[id] = cached - return s.cacheCaCertificates, s.cacheCaPrivateKeys, nil + return cached.certificates, cached.privateKeys, nil } func BuildCAX509Template() *x509.Certificate { @@ -117,7 +123,7 @@ func BuildCAX509Template() *x509.Certificate { // Creates and stores CA keypair // Should be called with the mutex held, to prevent concurrent creation of different keys -func (c *VFSCAStore) generateCACertificate() (*certificates, *privateKeys, error) { +func (c *VFSCAStore) generateCACertificate(id string) (*certificates, *privateKeys, error) { template := BuildCAX509Template() caRsaKey, err := rsa.GenerateKey(crypto_rand.Reader, 2048) @@ -135,14 +141,14 @@ func (c *VFSCAStore) generateCACertificate() (*certificates, *privateKeys, error t := time.Now().UnixNano() serial := pki.BuildPKISerial(t) - keyPath := c.buildPrivateKeyPath(CertificateId_CA, serial) + keyPath := c.buildPrivateKeyPath(id, serial) err = c.storePrivateKey(caPrivateKey, keyPath) if err != nil { return nil, nil, err } // Make double-sure it round-trips - privateKeys, err := c.loadPrivateKeys(c.buildPrivateKeyPoolPath(CertificateId_CA)) + privateKeys, err := c.loadPrivateKeys(c.buildPrivateKeyPoolPath(id)) if err != nil { return nil, nil, err } @@ -150,14 +156,14 @@ func (c *VFSCAStore) generateCACertificate() (*certificates, *privateKeys, error return nil, nil, fmt.Errorf("failed to round-trip CA private key") } - certPath := c.buildCertificatePath(CertificateId_CA, serial) + certPath := c.buildCertificatePath(id, serial) err = c.storeCertificate(caCertificate, certPath) if err != nil { return nil, nil, err } // Make double-sure it round-trips - certificates, err := c.loadCertificates(c.buildCertificatePoolPath(CertificateId_CA)) + certificates, err := c.loadCertificates(c.buildCertificatePoolPath(id)) if err != nil { return nil, nil, err } @@ -414,25 +420,34 @@ func (c *VFSCAStore) MirrorTo(basedir vfs.Path) error { return vfs.CopyTree(c.basedir, basedir) } -func (c *VFSCAStore) IssueCert(id string, serial *big.Int, privateKey *pki.PrivateKey, template *x509.Certificate) (*pki.Certificate, error) { +func (c *VFSCAStore) IssueCert(signer string, id string, serial *big.Int, privateKey *pki.PrivateKey, template *x509.Certificate) (*pki.Certificate, error) { glog.Infof("Issuing new certificate: %q", id) template.SerialNumber = serial - caCertificates, caPrivateKeys, err := c.readCAKeypairs() - if err != nil { - return nil, err - } + var cert *pki.Certificate + if template.IsCA { + var err error + cert, err = pki.SignNewCertificate(privateKey, template, nil, nil) + if err != nil { + return nil, err + } + } else { + caCertificates, caPrivateKeys, err := c.readCAKeypairs(signer) + if err != nil { + return nil, err + } - if caPrivateKeys == nil || caPrivateKeys.Primary() == nil { - return nil, fmt.Errorf("ca.key was not found; cannot issue certificates") - } - cert, err := pki.SignNewCertificate(privateKey, template, caCertificates.Primary().Certificate, caPrivateKeys.Primary()) - if err != nil { - return nil, err + if caPrivateKeys == nil || caPrivateKeys.Primary() == nil { + return nil, fmt.Errorf("ca key for %q was not found; cannot issue certificates", signer) + } + cert, err = pki.SignNewCertificate(privateKey, template, caCertificates.Primary().Certificate, caPrivateKeys.Primary()) + if err != nil { + return nil, err + } } - err = c.StoreKeypair(id, cert, privateKey) + err := c.StoreKeypair(id, cert, privateKey) if err != nil { return nil, err } @@ -557,7 +572,7 @@ func (c *VFSCAStore) loadOnePrivateKey(p vfs.Path) (*pki.PrivateKey, error) { func (c *VFSCAStore) FindPrivateKey(id string) (*pki.PrivateKey, error) { var keys *privateKeys if id == CertificateId_CA { - _, caPrivateKeys, err := c.readCAKeypairs() + _, caPrivateKeys, err := c.readCAKeypairs(id) if err != nil { return nil, err } @@ -592,10 +607,10 @@ func (c *VFSCAStore) PrivateKey(id string, createIfMissing bool) (*pki.PrivateKe } -func (c *VFSCAStore) CreateKeypair(id string, template *x509.Certificate, privateKey *pki.PrivateKey) (*pki.Certificate, error) { +func (c *VFSCAStore) CreateKeypair(signer string, id string, template *x509.Certificate, privateKey *pki.PrivateKey) (*pki.Certificate, error) { serial := c.buildSerial() - cert, err := c.IssueCert(id, serial, privateKey, template) + cert, err := c.IssueCert(signer, id, serial, privateKey, template) if err != nil { return nil, err }