Verify CA keypair IDs for kops-controller-issued certs

This commit is contained in:
John Gardiner Myers 2021-07-12 22:03:43 -07:00
parent 2526a35962
commit 191df58267
11 changed files with 81 additions and 27 deletions

View File

@ -19,5 +19,6 @@ go_library(
"//util/pkg/vfs:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library",
"//vendor/k8s.io/klog/v2:go_default_library",
"//vendor/sigs.k8s.io/yaml:go_default_library",
],
)

View File

@ -22,6 +22,7 @@ import (
"path"
"k8s.io/kops/pkg/pki"
"sigs.k8s.io/yaml"
)
type keystore struct {
@ -43,27 +44,27 @@ func (k keystore) FindPrimaryKeypair(name string) (*pki.Certificate, *pki.Privat
return entry.certificate, entry.key, nil
}
func newKeystore(basePath string, cas []string) (pki.Keystore, error) {
func newKeystore(basePath string, cas []string) (pki.Keystore, map[string]string, error) {
keystore := &keystore{
keys: map[string]keystoreEntry{},
}
for _, name := range cas {
certBytes, err := ioutil.ReadFile(path.Join(basePath, name+".crt"))
if err != nil {
return nil, fmt.Errorf("reading %q certificate: %v", name, err)
return nil, nil, fmt.Errorf("reading %q certificate: %v", name, err)
}
certificate, err := pki.ParsePEMCertificate(certBytes)
if err != nil {
return nil, fmt.Errorf("parsing %q certificate: %v", name, err)
return nil, nil, fmt.Errorf("parsing %q certificate: %v", name, err)
}
keyBytes, err := ioutil.ReadFile(path.Join(basePath, name+".key"))
if err != nil {
return nil, fmt.Errorf("reading %q key: %v", name, err)
return nil, nil, fmt.Errorf("reading %q key: %v", name, err)
}
key, err := pki.ParsePEMPrivateKey(keyBytes)
if err != nil {
return nil, fmt.Errorf("parsing %q key: %v", name, err)
return nil, nil, fmt.Errorf("parsing %q key: %v", name, err)
}
keystore.keys[name] = keystoreEntry{
@ -72,5 +73,15 @@ func newKeystore(basePath string, cas []string) (pki.Keystore, error) {
}
}
return keystore, nil
var keypairIDs map[string]string
keypairIDsBytes, err := ioutil.ReadFile(path.Join(basePath, "keypair-ids.yaml"))
if err != nil {
return nil, nil, fmt.Errorf("reading keypair-ids.yaml")
}
err = yaml.Unmarshal(keypairIDsBytes, &keypairIDs)
if err != nil {
return nil, nil, fmt.Errorf("parsing keypair-ids.yaml")
}
return keystore, keypairIDs, nil
}

View File

@ -40,11 +40,12 @@ import (
)
type Server struct {
opt *config.Options
certNames sets.String
server *http.Server
verifier fi.Verifier
keystore pki.Keystore
opt *config.Options
certNames sets.String
keypairIDs map[string]string
server *http.Server
verifier fi.Verifier
keystore pki.Keystore
// configBase is the base of the configuration storage.
configBase vfs.Path
@ -81,7 +82,7 @@ func NewServer(opt *config.Options, verifier fi.Verifier) (*Server, error) {
func (s *Server) Start() error {
var err error
s.keystore, err = newKeystore(s.opt.Server.CABasePath, s.opt.Server.SigningCAs)
s.keystore, s.keypairIDs, err = newKeystore(s.opt.Server.CABasePath, s.opt.Server.SigningCAs)
if err != nil {
return err
}
@ -152,7 +153,7 @@ func (s *Server) bootstrap(w http.ResponseWriter, r *http.Request) {
validHours := (455 * 24) + (hash.Sum32() % (30 * 24))
for name, pubKey := range req.Certs {
cert, err := s.issueCert(name, pubKey, id, validHours)
cert, err := s.issueCert(name, pubKey, id, validHours, req.KeypairIDs)
if err != nil {
klog.Infof("bootstrap %s cert %q issue err: %v", r.RemoteAddr, name, err)
w.WriteHeader(http.StatusBadRequest)
@ -167,7 +168,7 @@ func (s *Server) bootstrap(w http.ResponseWriter, r *http.Request) {
klog.Infof("bootstrap %s %s success", r.RemoteAddr, id.NodeName)
}
func (s *Server) issueCert(name string, pubKey string, id *fi.VerifyResult, validHours uint32) (string, error) {
func (s *Server) issueCert(name string, pubKey string, id *fi.VerifyResult, validHours uint32, keypairIDs map[string]string) (string, error) {
block, _ := pem.Decode([]byte(pubKey))
if block.Type != "RSA PUBLIC KEY" {
return "", fmt.Errorf("unexpected key type %q", block.Type)
@ -216,6 +217,13 @@ func (s *Server) issueCert(name string, pubKey string, id *fi.VerifyResult, vali
return "", fmt.Errorf("unexpected key name")
}
// This field was added to the protocol in kOps 1.22.
if len(keypairIDs) > 0 {
if keypairIDs[issueReq.Signer] != s.keypairIDs[issueReq.Signer] {
return "", fmt.Errorf("request's keypair ID %q for %s didn't match server's %q", keypairIDs[issueReq.Signer], issueReq.Signer, s.keypairIDs[issueReq.Signer])
}
}
cert, _, _, err := pki.IssueCert(issueReq, s.keystore)
if err != nil {
return "", fmt.Errorf("issuing certificate: %v", err)

View File

@ -64,8 +64,9 @@ func (b BootstrapClientBuilder) Build(c *fi.ModelBuilderContext) error {
}
bootstrapClientTask := &nodetasks.BootstrapClientTask{
Client: bootstrapClient,
Certs: b.bootstrapCerts,
Client: bootstrapClient,
Certs: b.bootstrapCerts,
KeypairIDs: b.bootstrapKeypairIDs,
}
for _, cert := range b.bootstrapCerts {

View File

@ -65,8 +65,9 @@ type NodeupModelContext struct {
// HasAPIServer is true if the InstanceGroup has a role of master or apiserver (pupulated by Init)
HasAPIServer bool
kubernetesVersion semver.Version
bootstrapCerts map[string]*nodetasks.BootstrapCert
kubernetesVersion semver.Version
bootstrapCerts map[string]*nodetasks.BootstrapCert
bootstrapKeypairIDs map[string]string
// ConfigurationMode determines if we are prewarming an instance or running it live
ConfigurationMode string
@ -81,6 +82,7 @@ func (c *NodeupModelContext) Init() error {
}
c.kubernetesVersion = *k8sVersion
c.bootstrapCerts = map[string]*nodetasks.BootstrapCert{}
c.bootstrapKeypairIDs = map[string]string{}
role := c.BootConfig.InstanceGroupRole
@ -240,7 +242,7 @@ func (c *NodeupModelContext) BuildIssuedKubeconfig(name string, subject nodetask
}
// GetBootstrapCert requests a certificate keypair from kops-controller.
func (c *NodeupModelContext) GetBootstrapCert(name string) (cert, key fi.Resource) {
func (c *NodeupModelContext) GetBootstrapCert(name string, signer string) (cert, key fi.Resource, err error) {
if c.IsMaster {
panic("control plane nodes can't get certs from kops-controller")
}
@ -252,13 +254,20 @@ func (c *NodeupModelContext) GetBootstrapCert(name string) (cert, key fi.Resourc
}
c.bootstrapCerts[name] = b
}
return b.Cert, b.Key
c.bootstrapKeypairIDs[signer] = c.NodeupConfig.KeypairIDs[signer]
if c.bootstrapKeypairIDs[signer] == "" {
return nil, nil, fmt.Errorf("no keypairID for %q", signer)
}
return b.Cert, b.Key, nil
}
// BuildBootstrapKubeconfig generates a kubeconfig with a client certificate from either kops-controller or the state store.
func (c *NodeupModelContext) BuildBootstrapKubeconfig(name string, ctx *fi.ModelBuilderContext) (fi.Resource, error) {
if c.UseKopsControllerForNodeBootstrap() {
cert, key := c.GetBootstrapCert(name)
cert, key, err := c.GetBootstrapCert(name, fi.CertificateIDCA)
if err != nil {
return nil, err
}
kubeConfig := &nodetasks.KubeConfig{
Name: name,
@ -273,7 +282,7 @@ func (c *NodeupModelContext) BuildBootstrapKubeconfig(name string, ctx *fi.Model
kubeConfig.ServerURL = "https://" + c.Cluster.Spec.MasterInternalName
}
err := ctx.EnsureTask(kubeConfig)
err = ctx.EnsureTask(kubeConfig)
if err != nil {
return nil, err
}

View File

@ -23,6 +23,7 @@ import (
"k8s.io/kops/pkg/wellknownusers"
"k8s.io/kops/upup/pkg/fi"
"k8s.io/kops/upup/pkg/fi/nodeup/nodetasks"
"sigs.k8s.io/yaml"
)
// KopsControllerBuilder installs the keys for a kops-controller.
@ -91,5 +92,17 @@ func (b *KopsControllerBuilder) Build(c *fi.ModelBuilderContext) error {
}
}
keypairIDs, err := yaml.Marshal(b.NodeupConfig.KeypairIDs)
if err != nil {
return err
}
c.AddTask(&nodetasks.File{
Path: filepath.Join(pkiDir, "keypair-ids.yaml"),
Contents: fi.NewBytesResource(keypairIDs),
Type: nodetasks.FileType_File,
Mode: s("0600"),
Owner: s(wellknownusers.KopsControllerName),
})
return nil
}

View File

@ -559,7 +559,10 @@ func (b *KubeletBuilder) buildKubeletServingCertificate(c *fi.ModelBuilderContex
}
if !b.HasAPIServer {
cert, key := b.GetBootstrapCert(name)
cert, key, err := b.GetBootstrapCert(name, fi.CertificateIDCA)
if err != nil {
return err
}
c.AddTask(&nodetasks.File{
Path: filepath.Join(dir, name+".crt"),

View File

@ -123,7 +123,10 @@ func (b *CiliumBuilder) buildCiliumEtcdSecrets(c *fi.ModelBuilderContext) error
return issueCert.AddFileTasks(c, dir, name, "", nil)
} else {
if b.UseKopsControllerForNodeBootstrap() {
cert, key := b.GetBootstrapCert(name)
cert, key, err := b.GetBootstrapCert(name, signer)
if err != nil {
return err
}
c.AddTask(&nodetasks.File{
Path: filepath.Join(dir, name+".crt"),

View File

@ -24,6 +24,8 @@ type BootstrapRequest struct {
APIVersion string `json:"apiVersion"`
// Certs are the requested certificates and their respective public keys.
Certs map[string]string `json:"certs"`
// KeypairIDs are the keypair IDs of the CAs to use for issuing certificates.
KeypairIDs map[string]string `json:"keypairIDs"`
// IncludeNodeConfig controls whether the cluster & instance group configuration should be returned.
// This allows for nodes without access to the kops state store.

View File

@ -35,6 +35,7 @@ import (
"k8s.io/klog/v2"
kopsbase "k8s.io/kops"
"k8s.io/kops/pkg/apis/kops"
apiModel "k8s.io/kops/pkg/apis/kops/model"
"k8s.io/kops/pkg/apis/kops/registry"
"k8s.io/kops/pkg/apis/kops/util"
"k8s.io/kops/pkg/apis/kops/validation"
@ -1295,17 +1296,16 @@ func (n *nodeUpConfigBuilder) BuildConfig(ig *kops.InstanceGroup, apiserverAddit
}
}
if err := getTasksCertificate(caTasks, fi.CertificateIDCA, config, false); err != nil {
if err := getTasksCertificate(caTasks, fi.CertificateIDCA, config, true); err != nil {
return nil, nil, err
}
if caTasks["etcd-clients-ca-cilium"] != nil {
if err := getTasksCertificate(caTasks, "etcd-clients-ca-cilium", config, hasAPIServer); err != nil {
if err := getTasksCertificate(caTasks, "etcd-clients-ca-cilium", config, hasAPIServer || apiModel.UseKopsControllerForNodeBootstrap(n.cluster)); err != nil {
return nil, nil, err
}
}
if isMaster {
config.KeypairIDs[fi.CertificateIDCA] = caTasks[fi.CertificateIDCA].Keyset().Primary.Id
if err := getTasksCertificate(caTasks, "etcd-clients-ca", config, true); err != nil {
return nil, nil, err
}

View File

@ -41,6 +41,8 @@ import (
type BootstrapClientTask struct {
// Certs are the requested certificates.
Certs map[string]*BootstrapCert
// KeypairIDs are the keypair IDs of the CAs to use for issuing certificates.
KeypairIDs map[string]string
// Client holds the client wrapper for the kops-bootstrap protocol
Client *KopsBootstrapClient
@ -83,6 +85,7 @@ func (b *BootstrapClientTask) Run(c *fi.Context) error {
req := nodeup.BootstrapRequest{
APIVersion: nodeup.BootstrapAPIVersion,
Certs: map[string]string{},
KeypairIDs: b.KeypairIDs,
}
if b.keys == nil {