diff --git a/cli/cmd/set.go b/cli/cmd/set.go index 6681181d35..eaaf5e2422 100644 --- a/cli/cmd/set.go +++ b/cli/cmd/set.go @@ -44,7 +44,7 @@ func NewSetCmd() *cobra.Command { reference values embedded into the CLI. After the connection is established, the manifest is set. The Coordinator - will re-generate the mesh root certificate and accept new workloads to + will re-generate the mesh CA certificate and accept new workloads to issuer certificates. `, RunE: runSet, diff --git a/coordinator/meshapi.go b/coordinator/meshapi.go index 4eb117ea93..e03a03303a 100644 --- a/coordinator/meshapi.go +++ b/coordinator/meshapi.go @@ -74,12 +74,12 @@ func (i *meshAPIServer) NewMeshCert(_ context.Context, req *meshapi.NewMeshCertR "getting certificate with public key hash %q: %v", req.PeerPublicKeyHash, err) } - meshCACert := i.caChainGetter.GetMeshRootCert() - intermCert := i.caChainGetter.GetIntermCert() + meshCACert := i.caChainGetter.GetMeshCACert() + intermCACert := i.caChainGetter.GetIntermCACert() return &meshapi.NewMeshCertResponse{ MeshCACert: meshCACert, - CertChain: append(cert, intermCert...), + CertChain: append(cert, intermCACert...), RootCACert: i.caChainGetter.GetCoordinatorRootCert(), }, nil } diff --git a/coordinator/userapi.go b/coordinator/userapi.go index f2513c0570..3a420e7cd2 100644 --- a/coordinator/userapi.go +++ b/coordinator/userapi.go @@ -102,7 +102,7 @@ func (s *userAPIServer) SetManifest(ctx context.Context, req *userapi.SetManifes resp := &userapi.SetManifestResponse{ CoordinatorRoot: s.caChainGetter.GetCoordinatorRootCert(), - MeshRoot: s.caChainGetter.GetMeshRootCert(), + MeshRoot: s.caChainGetter.GetMeshCACert(), } s.logger.Info("SetManifest succeeded") @@ -134,7 +134,7 @@ func (s *userAPIServer) GetManifests(_ context.Context, _ *userapi.GetManifestsR Manifests: manifestBytes, Policies: policySliceToBytesSlice(policies), CoordinatorRoot: s.caChainGetter.GetCoordinatorRootCert(), - MeshRoot: s.caChainGetter.GetMeshRootCert(), + MeshRoot: s.caChainGetter.GetMeshCACert(), } s.logger.Info("GetManifest succeeded") @@ -211,8 +211,8 @@ func manifestSliceToBytesSlice(s []*manifest.Manifest) ([][]byte, error) { type certChainGetter interface { GetCoordinatorRootCert() []byte - GetMeshRootCert() []byte - GetIntermCert() []byte + GetMeshCACert() []byte + GetIntermCACert() []byte } type manifestSetGetter interface { diff --git a/coordinator/userapi_test.go b/coordinator/userapi_test.go index bf8ba0a835..16cd0157a0 100644 --- a/coordinator/userapi_test.go +++ b/coordinator/userapi_test.go @@ -395,8 +395,8 @@ func (s *stubManifestSetGetter) LatestManifest() (*manifest.Manifest, error) { type stubCertChainGetter struct{} func (s *stubCertChainGetter) GetCoordinatorRootCert() []byte { return []byte("root") } -func (s *stubCertChainGetter) GetMeshRootCert() []byte { return []byte("mesh") } -func (s *stubCertChainGetter) GetIntermCert() []byte { return []byte("inter") } +func (s *stubCertChainGetter) GetMeshCACert() []byte { return []byte("mesh") } +func (s *stubCertChainGetter) GetIntermCACert() []byte { return []byte("inter") } func rpcContext(key *ecdsa.PrivateKey) context.Context { var peerCertificates []*x509.Certificate diff --git a/docs/docs/_media/contrast_pki.drawio.svg b/docs/docs/_media/contrast_pki.drawio.svg new file mode 100644 index 0000000000..a8ade2ad29 --- /dev/null +++ b/docs/docs/_media/contrast_pki.drawio.svg @@ -0,0 +1,4 @@ + + + +
Root CA








Root CA Certificate
Root CA
Private Key
Intermediate CA








Intermediate CA
Certificate
Intermediate CA
Private Key
Mesh CA
Certificate
Mesh








Mesh Certificate
Mesh Private Key
signs
signs
signs
signs
\ No newline at end of file diff --git a/docs/docs/architecture/certificates-and-identities/pki.md b/docs/docs/architecture/certificates-and-identities/pki.md deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/docs/docs/architecture/certificates.md b/docs/docs/architecture/certificates.md new file mode 100644 index 0000000000..7b7f11d418 --- /dev/null +++ b/docs/docs/architecture/certificates.md @@ -0,0 +1,69 @@ +# Certificate authority + +The Coordinator acts as a certificate authority (CA) for the workloads +defined in the manifest. +After a workload pod's attestation has been verified by the Coordinator, +it receives a mesh certificate and the mesh CA certificate. +The mesh certificate can be used for example in a TLS connection as the server or +client certificate to proof to the other party that the workload has been +verified by the Coordinator. The other party can verify the mesh certificate +with the mesh CA certificate. While the certificates can be used by the workload +developer in different ways, they're automatically used in Contrast's service +mesh to establish mTLS connections between workloads in the same deployment. + +## Public key infrastructure + +The Coordinator establishes a public key infrastructure (PKI) for all workloads +contained in the manifest. The Coordinator holds three certificates: the root CA +certificate, the intermediate certificate and the mesh CA certificate. +The root CA certificate is a long-lasting certificate and it's private key signs +the intermediate certificate. The intermediate certificate and the mesh CA +certificate share the same private key. This intermediate private key is used +to sign the mesh certificates. Moreover, the intermediate private key and +therefore the intermediate certificate and the mesh CA certificate are +rotated when setting a new manifest. + +![PKI certificate chain](../_media/contrast_pki.drawio.svg) + +## Certificate rotation + +Depending on the configuration of the first manifest, it allows the workload +owner to update the manifest and therefore the deployment. To protect against +the introduction of a malicious container, every time the manifest is updated, +the Coordinator rotates the intermediate private key and therefore the +intermediate CA certificate and mesh CA certificate. If the user decides to not +trust the workload owner, they use the mesh CA certificate from the time they +verified the Coordinator and the manifest. This ensures that the user only +connects to workloads defined the manifest they verified since only those +workloads' certificates are singed with this intermediate private key. + +Similarly, the service mesh also uses the mesh CA certificate from the time +where the workload was started so the workload doesn't assume that an endpoint +is trusted just because the endpoint has a certificate created by the +coordinator. + +## Usage of the different certificates + +- The **root CA certificate** is returned when verifying the Coordinator. It can +be used by the data owner to verify the mesh certificates of the workloads. +This should only be used, if the data owner trusts all future updates to the +manifest and workloads. This is for instance the case when the workload owner is +the same person as the data owner. +- The **mesh CA certificate** is returned when verifying the Coordinator. It can +be used by the data owner to verify the mesh certificates of the workloads. +This certificate is bound to the manifest set when the Coordinator was verified. +If the manifest is updated, new workloads will receive mesh certificates that +are _not_ signed by the already retrieved mesh CA certificate certificate. +Instead, the Coordinator with the new manifest needs to be verified to retrieve +the new mesh CA certificate. This certificate is also used by the service mesh +to verify the mesh certificates. +- The **intermediate CA certificate** links the root CA certificate to the +mesh certificate so that the mesh certificate can be verified with the root CA +certificate. It's part of the certificate chain handed out by +endpoints in the service mesh. +- The **mesh certificate** is part of the certificate chain handed out by +endpoints in the service mesh. During the startup of a pod, the Initializer +requests a certificate from the Coordinator. If the Coordinator successfully +verifies the workload, it sends back this mesh certificate. The mesh certificate +contains X.509 extensions with information from the workloads attestation +document. diff --git a/docs/docs/examples/emojivoto.md b/docs/docs/examples/emojivoto.md index 84cf2f7cc4..2f3b9cc4f5 100644 --- a/docs/docs/examples/emojivoto.md +++ b/docs/docs/examples/emojivoto.md @@ -6,7 +6,6 @@ **This tutorial guides you through deploying [emojivoto](https://github.com/BuoyantIO/emojivoto) as a confidential Contrast deployment and validating the deployment from a voters perspective.** - Emojivoto is an example app allowing users to vote for different emojis and view votes on a leader board. It has a microservice architecture consisting of a web frontend (`web`), a gRPC backend for listing available emojis (`emoji`), and a backend for @@ -120,8 +119,8 @@ contrast verify -c "${coordinator}:1313" The CLI will attest the Coordinator using embedded reference values. If the command succeeds, the Coordinator deployment was successfully verified to be running in the expected Confidential Computing environment with the expected code version. The Coordinator will then return its -configuration over the established TLS channel. The CLI will store this information, namely the root -certificate of the mesh (`mesh-root.pem`) and the history of manifests, into the `verify/` directory. +configuration over the established TLS channel. The CLI will store this information, namely the CA +certificate of the mesh (`mesh-ca-cert.pem`) and the history of manifests, into the `verify/` directory. In addition, the policies referenced in the manifest history are also written into the same directory. ### Manifest history and artifact audit @@ -155,7 +154,7 @@ openssl s_client -CAfile verify/mesh-root.pem -verify_return_error -connect ${fr By default, mesh certificates are issued with a wildcard DNS entry. The web frontend is accessed via load balancer IP in this demo. Tools like curl check the certificate for IP entries in the SAN field. Validation fails since the certificate doesn't contain any IP entries as a subject alternative name (SAN). -For example, a connection attempt using the curl and the mesh root certificate with throw the following error: +For example, a connection attempt using the curl and the mesh CA certificate with throw the following error: ```sh $ curl --cacert ./verify/mesh-root.pem "https://${frontendIP}:443" @@ -187,11 +186,11 @@ Next, set the changed manifest at the coordinator with: contrast set -c "${coordinator}:1313" deployment/ ``` -The Contrast Coordinator will rotate the mesh root certificate on the manifest update. Workload certificates issued +The Contrast Coordinator will rotate the mesh CA certificate on the manifest update. Workload certificates issued after the manifest are thus issued by another certificate authority and services receiving the new CA certificate chain won't trust parts of the deployment that got their certificate issued before the update. This way, Contrast ensures that parts of the deployment that received a security update won't be infected by parts of the deployment at an older -patch level that may have been compromised. The `mesh-root.pem` is updated with the new CA certificate chain. +patch level that may have been compromised. The `mesh-CA-cert.pem` is updated with the new CA certificate chain. ### Rolling out the update diff --git a/docs/sidebars.js b/docs/sidebars.js index 1797da527d..c65773f1e9 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -161,18 +161,9 @@ const sidebars = { ] }, { - type: 'category', - label: 'Certificates and Identities', - link: { - type: 'generated-index', - }, - items: [ - { - type: 'doc', - label: 'PKI', - id: 'architecture/certificates-and-identities/pki', - }, - ] + type: 'doc', + label: 'Certificate authority', + id: 'architecture/certificates', }, { type: 'category', diff --git a/internal/ca/ca.go b/internal/ca/ca.go index 000ad57000..3b225f322d 100644 --- a/internal/ca/ca.go +++ b/internal/ca/ca.go @@ -19,9 +19,9 @@ import ( // CA is a cross-signing certificate authority. type CA struct { - rootPrivKey *ecdsa.PrivateKey - rootCert *x509.Certificate - rootPEM []byte + rootCAPrivKey *ecdsa.PrivateKey + rootCACert *x509.Certificate + rootCAPEM []byte // The intermPrivKey is used for both the intermediate and meshCA certificates. // This implements cross-signing for the leaf certificates. @@ -30,8 +30,8 @@ type CA struct { intermMux sync.RWMutex intermPrivKey *ecdsa.PrivateKey - intermCert *x509.Certificate - intermPEM []byte + intermCACert *x509.Certificate + intermCAPEM []byte meshCACert *x509.Certificate meshCAPEM []byte @@ -61,9 +61,9 @@ func New() (*CA, error) { } ca := CA{ - rootPrivKey: rootPrivKey, - rootCert: root, - rootPEM: rootPEM, + rootCAPrivKey: rootPrivKey, + rootCACert: root, + rootCAPEM: rootPEM, } if err := ca.RotateIntermCerts(); err != nil { return nil, fmt.Errorf("rotating intermediate certificates: %w", err) @@ -110,11 +110,11 @@ func (c *CA) NewAttestedMeshCert(names []string, extensions []pkix.Extension, su return certPEM, nil } -// RotateIntermCerts rotates the intermediate certificates. +// RotateIntermCerts rotates the intermediate CA and mesh CA certificate. // All existing mesh certificates will remain valid under the rootCA but -// not under the new intermediate certificate. -// To distribute the new intermediate certificate, all workloads should -// be restarted. +// not under the new intermediate CA and mesh CA certificates. +// To distribute the new intermediate CA and mesh CA certificates, all workloads +// should be restarted. func (c *CA) RotateIntermCerts() error { c.intermMux.Lock() defer c.intermMux.Unlock() @@ -122,7 +122,7 @@ func (c *CA) RotateIntermCerts() error { now := time.Now() notBefore := now.Add(-time.Hour) notAfter := now.AddDate(10, 0, 0) - c.intermCert = &x509.Certificate{ + c.intermCACert = &x509.Certificate{ Subject: pkix.Name{CommonName: "system:coordinator:meshCA"}, NotBefore: notBefore, NotAfter: notAfter, @@ -135,7 +135,7 @@ func (c *CA) RotateIntermCerts() error { if err != nil { return fmt.Errorf("generating intermediate private key: %w", err) } - c.intermPEM, err = createCert(c.intermCert, c.rootCert, &c.intermPrivKey.PublicKey, c.rootPrivKey) + c.intermCAPEM, err = createCert(c.intermCACert, c.rootCACert, &c.intermPrivKey.PublicKey, c.rootCAPrivKey) if err != nil { return fmt.Errorf("creating intermediate certificate: %w", err) } @@ -158,16 +158,16 @@ func (c *CA) RotateIntermCerts() error { // GetCoordinatorRootCert returns the root certificate of the CA in PEM format. func (c *CA) GetCoordinatorRootCert() []byte { - return c.rootPEM + return c.rootCAPEM } -// GetIntermCert returns the intermediate certificate of the CA in PEM format. -func (c *CA) GetIntermCert() []byte { - return c.intermPEM +// GetIntermCACert returns the intermediate certificate of the CA in PEM format. +func (c *CA) GetIntermCACert() []byte { + return c.intermCAPEM } -// GetMeshRootCert returns the mesh root certificate of the CA in PEM format. -func (c *CA) GetMeshRootCert() []byte { +// GetMeshCACert returns the mesh CA certificate of the CA in PEM format. +func (c *CA) GetMeshCACert() []byte { return c.meshCAPEM } diff --git a/internal/ca/ca_test.go b/internal/ca/ca_test.go index 3b18577c71..413662a9aa 100644 --- a/internal/ca/ca_test.go +++ b/internal/ca/ca_test.go @@ -22,18 +22,18 @@ func TestNewCA(t *testing.T) { ca, err := New() require.NoError(err) assert.NotNil(ca) - assert.NotNil(ca.rootPrivKey) - assert.NotNil(ca.rootCert) - assert.NotNil(ca.rootPEM) + assert.NotNil(ca.rootCAPrivKey) + assert.NotNil(ca.rootCACert) + assert.NotNil(ca.rootCAPEM) assert.NotNil(ca.intermPrivKey) - assert.NotNil(ca.intermCert) - assert.NotNil(ca.intermPEM) + assert.NotNil(ca.intermCACert) + assert.NotNil(ca.intermCAPEM) root := x509.NewCertPool() - ok := root.AppendCertsFromPEM(ca.rootPEM) + ok := root.AppendCertsFromPEM(ca.rootCAPEM) assert.True(ok) - cert := parsePEMCertificate(t, ca.intermPEM) + cert := parsePEMCertificate(t, ca.intermCAPEM) opts := x509.VerifyOptions{Roots: root} @@ -158,15 +158,15 @@ func TestRotateIntermCerts(t *testing.T) { ca, err := New() require.NoError(err) - oldIntermCert := ca.intermCert - oldintermPEM := ca.intermPEM + oldIntermCert := ca.intermCACert + oldintermPEM := ca.intermCAPEM oldMeshCACert := ca.meshCACert oldMeshCAPEM := ca.meshCAPEM err = ca.RotateIntermCerts() assert.NoError(err) - assert.NotEqual(oldIntermCert, ca.intermCert) - assert.NotEqual(oldintermPEM, ca.intermPEM) + assert.NotEqual(oldIntermCert, ca.intermCACert) + assert.NotEqual(oldintermPEM, ca.intermCAPEM) assert.NotEqual(oldMeshCACert, ca.meshCACert) assert.NotEqual(oldMeshCAPEM, ca.meshCAPEM) } @@ -181,11 +181,11 @@ func TestCAConcurrent(t *testing.T) { wg := sync.WaitGroup{} getIntermCert := func() { defer wg.Done() - assert.NotEmpty(ca.GetIntermCert()) + assert.NotEmpty(ca.GetIntermCACert()) } getMeshCACert := func() { defer wg.Done() - assert.NotEmpty(ca.GetMeshRootCert()) + assert.NotEmpty(ca.GetMeshCACert()) } getRootCACert := func() { defer wg.Done()