Skip to content

Commit

Permalink
Support external CA-Cert (#1307)
Browse files Browse the repository at this point in the history
* allow for external CA to be loaded via env Vars

* adding check for key and cert file existence

* Certificate Enhancements
adding keylength for CA
adding setting->certificateauthority->... config knobs to the topology file

* node based key length

* please deepsource

* rename KeyLength to KeySize

* please deepsource

* parsing validityDuration from config to time.Duration

* move cert authority settings under topology, add tests and add keysize to node

* recursive resolution of the nodes certificate struct

* removed unused struct

* rename explicit to external

* fix tests

* fix

* fix nil pointer

* added json schema

* update

* validity-duration for node certificates

* added docs

* added comments for merge function

* use BoolPointer util function

* remove different pointer bools

* added comments to settings structs

* remove WithExternalCA option

* added path resolve and env vars override

* bring back settings to global level and fix test paths

* fix internal CA tests

* use old return syntax

* docs for tools sign key size

* move CA init to certificateAuthoritySetup as it seems more appropriate

* bring back StoreNodeCert func

* removed unused receiver

* use cert.Write when storing the CA

* revert the change of storing ca with storeNodeCert

---------

Co-authored-by: Roman Dodin <[email protected]>
  • Loading branch information
steiler and hellt authored Aug 17, 2023
1 parent ac30f14 commit 024f005
Show file tree
Hide file tree
Showing 25 changed files with 684 additions and 99 deletions.
16 changes: 13 additions & 3 deletions cert/ca.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ func (ca *CA) GenerateCACert(input *CACSRInput) (*Certificate, error) {
}

// generate key
caPrivKey, err := rsa.GenerateKey(rand.Reader, 2048)
caPrivKey, err := rsa.GenerateKey(rand.Reader, input.KeySize)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -108,6 +108,16 @@ func (ca *CA) GenerateAndSignNodeCert(input *NodeCSRInput) (*Certificate, error)
// parse hosts from input to retrieve dns and ip SANs
dns, ip := parseHostsInput(input.Hosts)

keysize := 2048
if input.KeySize > 0 {
keysize = input.KeySize
}

expiry := time.Until(time.Now().AddDate(1, 0, 0)) // 1 year as default
if input.Expiry > 0 {
expiry = input.Expiry
}

certTemplate := &x509.Certificate{
RawSubject: []byte{},
SerialNumber: big.NewInt(1658),
Expand All @@ -121,13 +131,13 @@ func (ca *CA) GenerateAndSignNodeCert(input *NodeCSRInput) (*Certificate, error)
DNSNames: dns,
IPAddresses: ip,
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(1, 0, 0), // HARDCODED 1 year
NotAfter: time.Now().Add(expiry),
SubjectKeyId: []byte{1, 2, 3, 4, 6},
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
KeyUsage: x509.KeyUsageDigitalSignature,
}

newPrivKey, err := rsa.GenerateKey(rand.Reader, 2048)
newPrivKey, err := rsa.GenerateKey(rand.Reader, keysize)
if err != nil {
return nil, err
}
Expand Down
20 changes: 13 additions & 7 deletions cert/certificate.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,17 @@ func NewCertificateFromFile(certFilePath, keyFilePath, csrFilePath string) (*Cer
}

// CSR
_, err = os.Stat(csrFilePath)
if err != nil {
log.Debugf("failed loading csr %s, continuing anyways", csrFilePath)
} else {
cert.Csr, err = utils.ReadFileContent(csrFilePath)
// The CSR might not be there, which is not an issue, just skip it
if csrFilePath != "" {

_, err = os.Stat(csrFilePath)
if err != nil {
return nil, err
log.Debugf("failed loading csr %s, continuing anyways", csrFilePath)
} else {
cert.Csr, err = utils.ReadFileContent(csrFilePath)
if err != nil {
return nil, err
}
}
}

Expand Down Expand Up @@ -86,5 +90,7 @@ type CaPaths interface {
NodeCertKeyAbsFilename(identifier string) string
NodeCertCSRAbsFilename(identifier string) string
NodeTLSDir(string) string
CaDir() string
CaCertAbsFilename() string
CaKeyAbsFilename() string
CaCSRAbsFilename() string
}
2 changes: 2 additions & 0 deletions cert/csr_input.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ type CACSRInput struct {
Organization string
OrganizationUnit string
Expiry time.Duration
KeySize int
}

// NodeCSRInput struct.
Expand All @@ -21,4 +22,5 @@ type NodeCSRInput struct {
Organization string
OrganizationUnit string
Expiry time.Duration
KeySize int
}
12 changes: 9 additions & 3 deletions cert/local_dir_cert_storage.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package cert

import (
"path/filepath"

"github.com/srl-labs/containerlab/utils"
)

Expand All @@ -18,7 +20,7 @@ func NewLocalDirCertStorage(paths CaPaths) *LocalDirCertStorage {

// LoadCaCert loads the CA certificate from disk.
func (c *LocalDirCertStorage) LoadCaCert() (*Certificate, error) {
return c.LoadNodeCert(c.paths.CaDir())
return NewCertificateFromFile(c.paths.CaCertAbsFilename(), c.paths.CaKeyAbsFilename(), "")
}

// LoadNodeCert loads the node certificate from disk.
Expand All @@ -30,9 +32,13 @@ func (c *LocalDirCertStorage) LoadNodeCert(nodeName string) (*Certificate, error
return NewCertificateFromFile(certFilename, keyFilename, csrFilename)
}

// StoreCaCert stores the given CA certificate in a file in the baseFolder.
// StoreCaCert stores the given CA certificate, its key and CSR on disk.
func (c *LocalDirCertStorage) StoreCaCert(cert *Certificate) error {
return c.StoreNodeCert(c.paths.CaDir(), cert)
// CA cert/key/csr can only be stored in the labdir/.tls/ca dir,
// so we need to create it if it does not exist.
utils.CreateDirectory(filepath.Dir(c.paths.CaCertAbsFilename()), 0777)

return cert.Write(c.paths.CaCertAbsFilename(), c.paths.CaKeyAbsFilename(), c.paths.CaCSRAbsFilename())
}

// StoreNodeCert stores the given certificate in a file in the baseFolder.
Expand Down
4 changes: 0 additions & 4 deletions clab/clab.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,10 +199,6 @@ func NewContainerLab(opts ...ClabOption) (*CLab, error) {
var err error
if c.TopoPaths.TopologyFileIsSet() {
err = c.parseTopology()

// init the Cert storage and CA
c.Cert.CertStorage = cert.NewLocalDirCertStorage(c.TopoPaths)
c.Cert.CA = cert.NewCA()
}
return c, err
}
Expand Down
1 change: 1 addition & 0 deletions clab/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ type Config struct {
Name string `json:"name,omitempty"`
Prefix *string `json:"prefix,omitempty"`
Mgmt *types.MgmtNet `json:"mgmt,omitempty"`
Settings *types.Settings `json:"settings,omitempty"`
Topology *types.Topology `json:"topology,omitempty"`
// the debug flag value as passed via cli
// may be used by other packages to enable debug logging
Expand Down
105 changes: 81 additions & 24 deletions cmd/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,22 +93,7 @@ func deployFn(_ *cobra.Command, _ []string) error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

// handle CTRL-C signal
sig := make(chan os.Signal, 1)
signal.Notify(sig, os.Interrupt, syscall.SIGTERM)
go func() {
<-sig
log.Errorf("Caught CTRL-C. Stopping deployment and cleaning up!")
cancel()

// when interrupted, destroy the interrupted lab deployment with cleanup
cleanup = true
if err := destroyFn(destroyCmd, []string{}); err != nil {
log.Errorf("Failed to destroy lab: %v", err)
}

os.Exit(1) // skipcq: RVV-A0003
}()
setupCTRLCHandler(cancel)

opts := []clab.ClabOption{
clab.WithTimeout(timeout),
Expand All @@ -123,6 +108,7 @@ func deployFn(_ *cobra.Command, _ []string) error {
),
clab.WithDebug(debug),
}

c, err := clab.NewContainerLab(opts...)
if err != nil {
return err
Expand Down Expand Up @@ -183,14 +169,7 @@ func deployFn(_ *cobra.Command, _ []string) error {
return err
}

// define the attributes used to generate the CA Cert
caCertInput := &cert.CACSRInput{
CommonName: c.Config.Name + " lab CA",
Expiry: time.Until(time.Now().AddDate(1, 0, 0)), // should expire in a year from now
Organization: "containerlab",
}

if err := c.LoadOrGenerateCA(caCertInput); err != nil {
if err := certificateAuthoritySetup(c); err != nil {
return err
}

Expand Down Expand Up @@ -323,6 +302,84 @@ func deployFn(_ *cobra.Command, _ []string) error {
return printContainerInspect(containers, deployFormat)
}

// certificateAuthoritySetup sets up the certificate authority parameters.
func certificateAuthoritySetup(c *clab.CLab) error {
// init the Cert storage and CA
c.Cert.CertStorage = cert.NewLocalDirCertStorage(c.TopoPaths)
c.Cert.CA = cert.NewCA()

s := c.Config.Settings

// Set defaults for the CA parameters
keySize := 2048
validityDuration := time.Until(time.Now().AddDate(1, 0, 0)) // 1 year as default

// check that Settings.CertificateAuthority exists.
if s != nil && s.CertificateAuthority != nil {
// if ValidityDuration is set use the value
if s.CertificateAuthority.ValidityDuration != 0 {
validityDuration = s.CertificateAuthority.ValidityDuration
}

// if KeyLength is set use the value
if s.CertificateAuthority.KeySize != 0 {
keySize = s.CertificateAuthority.KeySize
}

// if external CA cert and and key are set, propagate to topopaths
extCACert := s.CertificateAuthority.Cert
extCAKey := s.CertificateAuthority.Key

// override external ca and key from env vars
if v := os.Getenv("CLAB_CA_KEY_FILE"); v != "" {
extCAKey = v
}

if v := os.Getenv("CLAB_CA_CERT_FILE"); v != "" {
extCACert = v
}

if extCACert != "" && extCAKey != "" {
err := c.TopoPaths.SetExternalCaFiles(extCACert, extCAKey)
if err != nil {
return err
}
}
}

// define the attributes used to generate the CA Cert
caCertInput := &cert.CACSRInput{
CommonName: c.Config.Name + " lab CA",
Expiry: validityDuration,
Organization: "containerlab",
KeySize: keySize,
}

return c.LoadOrGenerateCA(caCertInput)
}

// setupCTRLCHandler sets-up the handler for CTRL-C
// The deployment will be stopped and a destroy action is
// performed when interrupt signal is received.
func setupCTRLCHandler(cancel context.CancelFunc) {
// handle CTRL-C signal
sig := make(chan os.Signal, 1)
signal.Notify(sig, os.Interrupt, syscall.SIGTERM)
go func() {
<-sig
log.Errorf("Caught CTRL-C. Stopping deployment and cleaning up!")
cancel()

// when interrupted, destroy the interrupted lab deployment with cleanup
cleanup = true
if err := destroyFn(destroyCmd, []string{}); err != nil {
log.Errorf("Failed to destroy lab: %v", err)
}

os.Exit(1) // skipcq: RVV-A0003
}()
}

func setFlags(conf *clab.Config) {
if name != "" {
conf.Name = name
Expand Down
5 changes: 5 additions & 0 deletions cmd/tools_cert.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ var (
certHosts []string
caCertPath string
caKeyPath string
keySize int
)

func init() {
Expand Down Expand Up @@ -60,6 +61,7 @@ func init() {
signCertCmd.Flags().StringVarP(&path, "path", "p", "",
"path to write certificate and key to. Default is current working directory")
signCertCmd.Flags().StringVarP(&certNamePrefix, "name", "n", "cert", "certificate/key filename prefix")
signCertCmd.Flags().IntVarP(&keySize, "key-size", "", 2048, "private key size")
}

var certCmd = &cobra.Command{
Expand Down Expand Up @@ -111,6 +113,7 @@ func createCA(_ *cobra.Command, _ []string) error {
Organization: organization,
OrganizationUnit: organizationUnit,
Expiry: expDuration,
KeySize: keySize,
}

caCert, err := ca.GenerateCACert(csrInput)
Expand Down Expand Up @@ -149,6 +152,7 @@ func signCert(_ *cobra.Command, _ []string) error {

log.Debugf("CA cert path: %q", caCertPath)
if caCertPath != "" {
// TODO: we might also honor the External CA env vars here
caCert, err = cert.NewCertificateFromFile(caCertPath, caKeyPath, "")
if err != nil {
return err
Expand Down Expand Up @@ -178,6 +182,7 @@ func signCert(_ *cobra.Command, _ []string) error {
Organization: organization,
OrganizationUnit: organizationUnit,
Expiry: expDuration,
KeySize: keySize,
})
if err != nil {
return err
Expand Down
4 changes: 4 additions & 0 deletions docs/cmd/tools/cert/sign.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ Certificate Organization (O) field is set with `--organization | -o` flag. Defau

Certificate Organization Unit (OU) field is set with `--ou` flag. Defaults to `Containerlab Tools`.

### Key size

To set the key size, use the `--key-size` flag. Defaults to `2048`.

## Examples

```bash
Expand Down
56 changes: 51 additions & 5 deletions docs/manual/cert.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,59 @@
As more and more services move to "secure by default" behavior, it becomes important to simplify the PKI/TLS infrastructure provisioning in the lab environments. Containerlab embeds parts of [cfssl](https://github.com/cloudflare/cfssl) project to automate certificate generation and provisioning.
As more and more services move to "secure by default" behavior, it becomes important to simplify the PKI/TLS infrastructure provisioning in the lab environments.
Containerlab tries to ease the process of certificate provisioning providing the following features:

For [SR Linux](kinds/srl.md) nodes containerlab creates Certificate Authority (CA) and generates signed cert and key for each node of a lab. This makes SR Linux node to boot up with TLS profiles correctly configured and enable operation of a secured management protocol - gNMI.
1. Automated certificate provisioning for lab nodes.
2. Simplified CLI for CA and end-node keys generation.
3. Ability to use custom/external CA.

## Automated certificate provisioning

Automated certificate provisioning is a two-stage process. First, containerlab creates a Certificate Authority (CA) and generates a certificate and key for it, storing these artifacts in a [lab directory](conf-artifacts.md) in the `.tls` directory. Then, containerlab generates a certificate and key for each node of a lab and signs it with the CA. The signed certificate and key are then installed on the node.

!!!note
For other nodes the automated TLS pipeline is not provided yet and can be addressed by contributors.
Currently, automated installation of a node certificate is implemented only for [Nokia SR Linux](kinds/srl.md).

### CA certificate

When generating CA certificate and key, containerlab can take in the following optional parameters:

* `.settings.certificate-authority.key-size` - the size of the key in bytes, default is 2048
* `.settings.certificate-authority.validity-duration` - the duration of the certificate. For example: `10m`, `1000h`. Max unit is hour. Default is `8760h` (1 year)

### Node certificates

The decision to generate node certificates is driven by either of the following two parameters:

1. node kind
2. `issue` boolean parameter under `node-name.certificate` section.

For SR Linux nodes the `issue` parameter is set to `true` and can't be changed. For other node kinds the `issue` parameter is set to `false` by default and can be [overridden](nodes.md#certificate) by the user.

Apart from automated pipeline for certificate provisioning, containerlab exposes the following commands that can create a CA and node's cert/key:
## Simplified CLI for CA and end-node keys generation

Apart automated pipeline for certificate provisioning, containerlab exposes the following commands that can create a CA and node's cert/key:

* [`tools cert ca create`](../cmd/tools/cert/ca/create.md) - creates a Certificate Authority
* [`tools cert sign`](../cmd/tools/cert/sign.md) - creates certificate/key for a host and signs the certificate with CA

With these two commands users can easily create CA node certificates and secure the transport channel of various protocols. [This lab](https://clabs.netdevops.me/security/gnmitls/) demonstrates how with containerlab's help one can easily create certificates and configure Nokia SR OS to use it for secured gNMI communication.
With these two commands users can easily create CA node certificates and secure the transport channel of various protocols. [This lab](https://clabs.netdevops.me/security/gnmitls/) demonstrates how with containerlab's help one can easily create certificates and configure Nokia SR OS to use it for secured gNMI communication.

## External CA

Users who require more control over the certificate generation process can use an existing external CA. Containerlab needs to be provided with the CA certificate and key. The CA certificate and key must be provided via `.settings.certificate-authority.[key]|[cert]` configuration parameters.

```yaml
name: ext-ca
settings:
certificate-authority:
cert: /path/to/ca.crt
key: /path/to/ca.key
```
When using an external CA, containerlab will not generate a CA certificate and key. Instead, it will use the provided CA certificate and key to sign the node certificates.
The paths can be provided in absolute or relative form. If the path is relative, it is relative to the directory where clab file is located.
In addition to setting External CA files via `settings` section, users can also set the following environment variables:

* `CLAB_CA_CERT_FILE` - path to the CA certificate
* `CLAB_CA_KEY_FILE` - path to the CA key
Loading

0 comments on commit 024f005

Please sign in to comment.