m/pkg/pki: refactor, allow for external certificates
The pki library supported managing certificates in two modes:
- default, when name != ""
- volatile/ephemeral, when name == ""
The difference between the two being that default certificates were
fully stored in etcd (key and x509 certificate), while volatile
certificates weren't stored at all. However, both kinds needed private
keys passed to the pki library.
We want to be able to emit certificates without having private keys for
that certificate, so we end up a third mode of operation: 'external
certificates'. These are still stored in etcd, but without any
corresponding private key.
In the future we might actually get rid of ephemeral certificates by
expanding the logic of external certificates to provide a full audit log
and revocation system, instead of matching by Certificate Name. But this
will do for now.
We also use this opportunity to write some simple tests for this
package.
Change-Id: I193f4b147273b0a3981c38d749b43362d3c1b69a
Reviewed-on: https://review.monogon.dev/c/monogon/+/263
Reviewed-by: Mateusz Zalega <mateusz@monogon.tech>
diff --git a/metropolis/node/core/cluster/cluster_bootstrap.go b/metropolis/node/core/cluster/cluster_bootstrap.go
index 30de8b3..16253bf 100644
--- a/metropolis/node/core/cluster/cluster_bootstrap.go
+++ b/metropolis/node/core/cluster/cluster_bootstrap.go
@@ -117,7 +117,7 @@
}
// And short-circuit creating the curator CA and node certificate.
- caCert, nodeCert, err := curator.BootstrapNodeCredentials(ctx, ckv, priv, pub)
+ caCert, nodeCert, err := curator.BootstrapNodeCredentials(ctx, ckv, pub)
if err != nil {
return fmt.Errorf("failed to bootstrap node credentials: %w", err)
}
diff --git a/metropolis/node/core/curator/bootstrap.go b/metropolis/node/core/curator/bootstrap.go
index 4e809dd..6147125 100644
--- a/metropolis/node/core/curator/bootstrap.go
+++ b/metropolis/node/core/curator/bootstrap.go
@@ -2,6 +2,7 @@
import (
"context"
+ "crypto/ed25519"
"fmt"
"go.etcd.io/etcd/clientv3"
@@ -24,19 +25,22 @@
// BootstrapNodeCredentials creates node credentials for the first node in a
// cluster. It can only be called by cluster bootstrap code. It returns the
// generated x509 CA and node certificates.
-//
-// TODO(q3k): don't require privkey, but that needs some //metropolis/pkg/pki changes first.
-func BootstrapNodeCredentials(ctx context.Context, etcd client.Namespaced, priv, pub []byte) (ca, node []byte, err error) {
- id := NodeID(pub)
+func BootstrapNodeCredentials(ctx context.Context, etcd client.Namespaced, pubkey ed25519.PublicKey) (ca, node []byte, err error) {
+ id := NodeID(pubkey)
- ca, _, err = pkiCA.Ensure(ctx, etcd)
+ ca, err = pkiCA.Ensure(ctx, etcd)
if err != nil {
err = fmt.Errorf("when ensuring CA: %w", err)
return
}
- nodeCert := pkiNamespace.New(pkiCA, "", pki.Server([]string{id}, nil))
- nodeCert.UseExistingKey(priv)
- node, _, err = nodeCert.Ensure(ctx, etcd)
+ nodeCert := &pki.Certificate{
+ Namespace: &pkiNamespace,
+ Issuer: pkiCA,
+ Template: pki.Server([]string{id}, nil),
+ Mode: pki.CertificateExternal,
+ PublicKey: pubkey,
+ }
+ node, err = nodeCert.Ensure(ctx, etcd)
if err != nil {
err = fmt.Errorf("when ensuring node cert: %w", err)
return
diff --git a/metropolis/node/core/curator/state_pki.go b/metropolis/node/core/curator/state_pki.go
index 70423ed..5c217c5 100644
--- a/metropolis/node/core/curator/state_pki.go
+++ b/metropolis/node/core/curator/state_pki.go
@@ -10,5 +10,10 @@
pkiNamespace = pki.Namespaced("/cluster-pki/")
// pkiCA is the main cluster CA, stored in etcd. It is used to emit cluster,
// node and user certificates.
- pkiCA = pkiNamespace.New(pki.SelfSigned, "cluster-ca", pki.CA("Metropolis Cluster CA"))
+ pkiCA = &pki.Certificate{
+ Namespace: &pkiNamespace,
+ Issuer: pki.SelfSigned,
+ Template: pki.CA("Metropolis Cluster CA"),
+ Name: "cluster-ca",
+ }
)
diff --git a/metropolis/node/kubernetes/pki/kubernetes.go b/metropolis/node/kubernetes/pki/kubernetes.go
index bb68907..a59ab98 100644
--- a/metropolis/node/kubernetes/pki/kubernetes.go
+++ b/metropolis/node/kubernetes/pki/kubernetes.go
@@ -106,10 +106,22 @@
}
make := func(i, name KubeCertificateName, template x509.Certificate) {
- pki.Certificates[name] = pki.namespace.New(pki.Certificates[i], string(name), template)
+ pki.Certificates[name] = &opki.Certificate{
+ Namespace: &pki.namespace,
+ Issuer: pki.Certificates[i],
+ Name: string(name),
+ Template: template,
+ Mode: opki.CertificateManaged,
+ }
}
- pki.Certificates[IdCA] = pki.namespace.New(opki.SelfSigned, string(IdCA), opki.CA("Metropolis Kubernetes ID CA"))
+ pki.Certificates[IdCA] = &opki.Certificate{
+ Namespace: &pki.namespace,
+ Issuer: opki.SelfSigned,
+ Name: string(IdCA),
+ Template: opki.CA("Metropolis Kubernetes ID CA"),
+ Mode: opki.CertificateManaged,
+ }
make(IdCA, APIServer, opki.Server(
[]string{
"kubernetes",
@@ -129,7 +141,13 @@
make(IdCA, Scheduler, opki.Server([]string{"kube-scheduler.local"}, nil))
make(IdCA, Master, opki.Client("metropolis:master", []string{"system:masters"}))
- pki.Certificates[AggregationCA] = pki.namespace.New(opki.SelfSigned, string(AggregationCA), opki.CA("Metropolis OpenAPI Aggregation CA"))
+ pki.Certificates[AggregationCA] = &opki.Certificate{
+ Namespace: &pki.namespace,
+ Issuer: opki.SelfSigned,
+ Name: string(AggregationCA),
+ Template: opki.CA("Metropolis OpenAPI Aggregation CA"),
+ Mode: opki.CertificateManaged,
+ }
make(AggregationCA, FrontProxyClient, opki.Client("front-proxy-client", nil))
return &pki
@@ -139,7 +157,7 @@
// are present on etcd.
func (k *PKI) EnsureAll(ctx context.Context) error {
for n, v := range k.Certificates {
- _, _, err := v.Ensure(ctx, k.KV)
+ _, err := v.Ensure(ctx, k.KV)
if err != nil {
return fmt.Errorf("could not ensure certificate %q exists: %w", n, err)
}
@@ -171,17 +189,26 @@
if !ok {
return nil, nil, fmt.Errorf("no certificate %q", name)
}
- return c.Ensure(ctx, k.KV)
+ cert, err = c.Ensure(ctx, k.KV)
+ if err != nil {
+ return
+ }
+ key, err = c.PrivateKeyX509()
+ return
}
// Kubeconfig generates a kubeconfig blob for this certificate. The same
// lifetime semantics as in .Ensure apply.
func Kubeconfig(ctx context.Context, kv clientv3.KV, c *opki.Certificate) ([]byte, error) {
- cert, key, err := c.Ensure(ctx, kv)
+ cert, err := c.Ensure(ctx, kv)
if err != nil {
return nil, fmt.Errorf("could not ensure certificate exists: %w", err)
}
+ key, err := c.PrivateKeyX509()
+ if err != nil {
+ return nil, fmt.Errorf("could not get certificate's private key: %w", err)
+ }
kubeconfig := configapi.NewConfig()
@@ -249,18 +276,28 @@
}
// VolatileKubelet returns a pair of server/client ceritficates for the Kubelet
-// to use. The certificates are volatile, meaning they are not stored in etcd,
+// to use. The certificates are ephemeral, meaning they are not stored in etcd,
// and instead are regenerated any time this function is called.
func (k *PKI) VolatileKubelet(ctx context.Context, name string) (server *opki.Certificate, client *opki.Certificate, err error) {
name = fmt.Sprintf("system:node:%s", name)
err = k.EnsureAll(ctx)
if err != nil {
- err = fmt.Errorf("could not ensure certificates exist: %w", err)
+ return nil, nil, fmt.Errorf("could not ensure certificates exist: %w", err)
}
kubeCA := k.Certificates[IdCA]
- server = k.namespace.New(kubeCA, "", opki.Server([]string{name}, nil))
- client = k.namespace.New(kubeCA, "", opki.Client(name, []string{"system:nodes"}))
- return
+ server = &opki.Certificate{
+ Namespace: &k.namespace,
+ Issuer: kubeCA,
+ Template: opki.Server([]string{name}, nil),
+ Mode: opki.CertificateEphemeral,
+ }
+ client = &opki.Certificate{
+ Namespace: &k.namespace,
+ Issuer: kubeCA,
+ Template: opki.Client(name, []string{"system:nodes"}),
+ Mode: opki.CertificateEphemeral,
+ }
+ return server, client, nil
}
// VolatileClient returns a client certificate for Kubernetes clients to use.
@@ -270,6 +307,10 @@
if err := k.EnsureAll(ctx); err != nil {
return nil, fmt.Errorf("could not ensure certificates exist: %w", err)
}
- kubeCA := k.Certificates[IdCA]
- return k.namespace.New(kubeCA, "", opki.Client(identity, groups)), nil
+ return &opki.Certificate{
+ Namespace: &k.namespace,
+ Issuer: k.Certificates[IdCA],
+ Template: opki.Client(identity, groups),
+ Mode: opki.CertificateEphemeral,
+ }, nil
}