diff --git a/coordinator/crypto/crypto.go b/coordinator/crypto/crypto.go index 3d57e85e..b91d5a48 100644 --- a/coordinator/crypto/crypto.go +++ b/coordinator/crypto/crypto.go @@ -23,7 +23,7 @@ import ( // GenerateCert creates a new certificate with the given parameters. // If privk is nil, a new private key is generated. func GenerateCert( - dnsNames []string, commonName string, privk *ecdsa.PrivateKey, + subjAltNames []string, commonName string, privk *ecdsa.PrivateKey, parentCertificate *x509.Certificate, parentPrivateKey *ecdsa.PrivateKey, ) (*x509.Certificate, *ecdsa.PrivateKey, error) { // Generate private key @@ -44,13 +44,15 @@ func GenerateCert( return nil, nil, fmt.Errorf("generating serial number: %w", err) } + additionalIPs, dnsNames := util.ExtractIPsFromAltNames(subjAltNames) + template := x509.Certificate{ SerialNumber: serialNumber, Subject: pkix.Name{ CommonName: commonName, }, DNSNames: dnsNames, - IPAddresses: util.DefaultCertificateIPAddresses, + IPAddresses: append(util.DefaultCertificateIPAddresses, additionalIPs...), NotBefore: notBefore, NotAfter: notAfter, diff --git a/docs/docs/architecture/coordinator.md b/docs/docs/architecture/coordinator.md index 16a54f0d..bd08260a 100644 --- a/docs/docs/architecture/coordinator.md +++ b/docs/docs/architecture/coordinator.md @@ -14,7 +14,7 @@ The Coordinator can be configured with several environment variables: * `EDG_COORDINATOR_MESH_ADDR`: The listener address for the gRPC server * `EDG_COORDINATOR_CLIENT_ADDR`: The listener address for the HTTP REST server -* `EDG_COORDINATOR_DNS_NAMES`: The DNS names for the cluster's root certificate +* `EDG_COORDINATOR_DNS_NAMES`: The DNS names and IPs for the cluster's root certificate * `EDG_COORDINATOR_SEAL_DIR`: The file path for storing sealed data When you use MarbleRun [with Kubernetes](../deployment/kubernetes.md), you can [scale the Coordinator to multiple instances](../features/recovery.md#distributed-coordinator) to increase availability and reduce the occurrence of events that require [manual recovery](../workflows/recover-coordinator.md). diff --git a/docs/docs/deployment/standalone.md b/docs/docs/deployment/standalone.md index 25725b81..f3677ac4 100644 --- a/docs/docs/deployment/standalone.md +++ b/docs/docs/deployment/standalone.md @@ -23,7 +23,7 @@ Per default, the Coordinator starts with the following default values. You can s | --- | --- | --- | | the listener address for the gRPC server | localhost:2001 | EDG_COORDINATOR_MESH_ADDR | | the listener address for the HTTP server | localhost: 4433 | EDG_COORDINATOR_CLIENT_ADDR | -| the DNS names for the cluster’s root certificate | localhost | EDG_COORDINATOR_DNS_NAMES | +| the DNS names and IPs for the cluster’s root certificate | localhost | EDG_COORDINATOR_DNS_NAMES | | the file path for storing sealed data | $PWD/marblerun-coordinator-data | EDG_COORDINATOR_SEAL_DIR | :::tip @@ -53,4 +53,4 @@ Per default, a Marble starts with the following default values. You can set your | network address of the Coordinator’s API for Marbles | `localhost:2001` | EDG_MARBLE_COORDINATOR_ADDR | | reference on one entry from your manifest’s `Marbles` section | - (this needs to be set every time) | EDG_MARBLE_TYPE | | local file path where the Marble stores its UUID | `$PWD/uuid` | EDG_MARBLE_UUID_FILE | -| DNS names the Coordinator will issue the Marble’s certificate for | `$EDG_MARBLE_TYPE` | EDG_MARBLE_DNS_NAMES | +| DNS names and IPs the Coordinator will issue the Marble’s certificate for | `$EDG_MARBLE_TYPE` | EDG_MARBLE_DNS_NAMES | diff --git a/docs/docs/workflows/add-service.md b/docs/docs/workflows/add-service.md index 79c0e2d0..568362d1 100644 --- a/docs/docs/workflows/add-service.md +++ b/docs/docs/workflows/add-service.md @@ -123,7 +123,7 @@ The environment variables have the following purposes. * `EDG_MARBLE_UUID_FILE` is the local file path where the Marble stores its UUID. Every instance of a Marble has its unique and public UUID. The file is needed to allow a Marble to restart under its UUID. -* `EDG_MARBLE_DNS_NAMES` is the list of DNS names the Coordinator will issue the Marble's certificate for. +* `EDG_MARBLE_DNS_NAMES` is the list of DNS names and IPs the Coordinator will issue the Marble's certificate for. ## **Step 4:** Deploy your service with Kubernetes diff --git a/util/tls.go b/util/tls.go index 354bd303..23115d9f 100644 --- a/util/tls.go +++ b/util/tls.go @@ -42,7 +42,7 @@ func MustGenerateTestMarbleCredentials() (cert *x509.Certificate, csrRaw []byte, } // GenerateCert generates a new self-signed certificate associated key-pair. -func GenerateCert(dnsNames []string, ipAddrs []net.IP, isCA bool) (*x509.Certificate, *ecdsa.PrivateKey, error) { +func GenerateCert(subjAltNames []string, ipAddrs []net.IP, isCA bool) (*x509.Certificate, *ecdsa.PrivateKey, error) { privk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { return nil, nil, err @@ -56,8 +56,8 @@ func GenerateCert(dnsNames []string, ipAddrs []net.IP, isCA bool) (*x509.Certifi return nil, nil, err } - // TODO: what else do we need to set here? - // Do we need x509.KeyUsageKeyEncipherment? + additionalIPs, dnsNames := ExtractIPsFromAltNames(subjAltNames) + template := x509.Certificate{ Subject: pkix.Name{ CommonName: marbleName, @@ -66,7 +66,7 @@ func GenerateCert(dnsNames []string, ipAddrs []net.IP, isCA bool) (*x509.Certifi NotBefore: notBefore, NotAfter: notAfter, DNSNames: dnsNames, - IPAddresses: ipAddrs, + IPAddresses: append(additionalIPs, ipAddrs...), KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyAgreement, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, @@ -86,10 +86,12 @@ func GenerateCert(dnsNames []string, ipAddrs []net.IP, isCA bool) (*x509.Certifi } // GenerateCSR generates a new CSR for the given DNSNames and private key. -func GenerateCSR(dnsNames []string, privk *ecdsa.PrivateKey) (*x509.CertificateRequest, error) { +func GenerateCSR(subjAltNames []string, privk *ecdsa.PrivateKey) (*x509.CertificateRequest, error) { + additionalIPs, dnsNames := ExtractIPsFromAltNames(subjAltNames) + template := x509.CertificateRequest{ DNSNames: dnsNames, - IPAddresses: DefaultCertificateIPAddresses, + IPAddresses: append(DefaultCertificateIPAddresses, additionalIPs...), } csrRaw, err := x509.CreateCertificateRequest(rand.Reader, &template, privk) if err != nil { @@ -122,3 +124,17 @@ func LoadGRPCTLSCredentials(cert *x509.Certificate, privk *ecdsa.PrivateKey, ins func TLSCertFromDER(certDER []byte, privk interface{}) *tls.Certificate { return &tls.Certificate{Certificate: [][]byte{certDER}, PrivateKey: privk} } + +// ExtractIPsFromAltNames extracts IP addresses and DNS names from a list of subject alternative names. +func ExtractIPsFromAltNames(subjAltNames []string) ([]net.IP, []string) { + var dnsNames []string + var additionalIPs []net.IP + for _, name := range subjAltNames { + if ip := net.ParseIP(name); ip != nil { + additionalIPs = append(additionalIPs, ip) + } else { + dnsNames = append(dnsNames, name) + } + } + return additionalIPs, dnsNames +} diff --git a/util/tls_test.go b/util/tls_test.go new file mode 100644 index 00000000..caf42f4e --- /dev/null +++ b/util/tls_test.go @@ -0,0 +1,52 @@ +// Copyright (c) Edgeless Systems GmbH. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +package util + +import ( + "net" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestExtractIPsFromAltNames(t *testing.T) { + testCases := map[string]struct { + altNames []string + wantIPs []net.IP + wantDNSNames []string + }{ + "empty": { + altNames: []string{}, + wantIPs: []net.IP{}, + wantDNSNames: []string{}, + }, + "only IPs": { + altNames: []string{"192.0.2.1", "192.0.2.15"}, + wantIPs: []net.IP{net.ParseIP("192.0.2.1"), net.ParseIP("192.0.2.15")}, + wantDNSNames: []string{}, + }, + "only DNS names": { + altNames: []string{"foo.bar", "example.com"}, + wantIPs: []net.IP{}, + wantDNSNames: []string{"foo.bar", "example.com"}, + }, + "mixed": { + altNames: []string{"192.0.2.1", "foo.bar"}, + wantIPs: []net.IP{net.ParseIP("192.0.2.1")}, + wantDNSNames: []string{"foo.bar"}, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + gotIPs, gotDNSNames := ExtractIPsFromAltNames(tc.altNames) + assert.ElementsMatch(tc.wantIPs, gotIPs) + assert.ElementsMatch(tc.wantDNSNames, gotDNSNames) + }) + } +}