Skip to content

Commit

Permalink
Add the ability to customise the details of the CA (#17309)
Browse files Browse the repository at this point in the history
Co-authored-by: James Rasell <[email protected]>
  • Loading branch information
lhaig and jrasell authored Jul 11, 2023
1 parent e178906 commit 1541358
Show file tree
Hide file tree
Showing 7 changed files with 330 additions and 48 deletions.
3 changes: 3 additions & 0 deletions .changelog/17309.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:improvement
cli: Add the ability to customize the details of the CA when running `nomad tls ca create`
```
124 changes: 116 additions & 8 deletions command/tls_ca_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,27 @@ type TLSCACreateCommand struct {
// additionalDomain provides a list of restricted domains to the CA which
// will then reject any domains other than these.
additionalDomain flags.StringFlag

// country is used to set a country code for the CA
country string

// postalCode is used to set a postal code for the CA
postalCode string

// province is used to set a province for the CA
province string

// locality is used to set a locality for the CA
locality string

// streetAddress is used to set a street address for the CA
streetAddress string

// organization is used to set an organization for the CA
organization string

// organizationalUnit is used to set an organizational unit for the CA
organizationalUnit string
}

func (c *TLSCACreateCommand) Help() string {
Expand All @@ -53,6 +74,9 @@ CA Create Options:
-common-name
Common Name of CA. Defaults to "Nomad Agent CA".
-country
Country of the CA. Defaults to "US".
-days
Provide number of days the CA is valid for from now on.
Defaults to 5 years or 1825 days.
Expand All @@ -61,24 +85,50 @@ CA Create Options:
Domain of Nomad cluster. Only used in combination with -name-constraint.
Defaults to "nomad".
-locality
Locality of the CA. Defaults to "San Francisco".
-name-constraint
Enables the DNS name restriction functionality to the CA. Results in the CA
rejecting certificates for any other DNS zone. If enabled, localhost and the
value of -domain will be added to the allowed DNS zones field. If the UI is
going to be served over HTTPS its hostname must be added with
-additional-domain. Defaults to false.
-organization
Organization of the CA. Defaults to "HashiCorp Inc.".
-organizational-unit
Organizational Unit of the CA. Defaults to "Nomad".
-postal-code
Postal Code of the CA. Defaults to "94105".
-province
Province of the CA. Defaults to "CA".
-street-address
Street Address of the CA. Defaults to "101 Second Street".
`
return strings.TrimSpace(helpText)
}

func (c *TLSCACreateCommand) AutocompleteFlags() complete.Flags {
return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient),
complete.Flags{
"-additional-domain": complete.PredictAnything,
"-common-name": complete.PredictAnything,
"-days": complete.PredictAnything,
"-domain": complete.PredictAnything,
"-name-constraint": complete.PredictAnything,
"-additional-domain": complete.PredictAnything,
"-common-name": complete.PredictAnything,
"-days": complete.PredictAnything,
"-country": complete.PredictAnything,
"-domain": complete.PredictAnything,
"-locality": complete.PredictAnything,
"-name-constraint": complete.PredictAnything,
"-organization": complete.PredictAnything,
"-organizational-unit": complete.PredictAnything,
"-postal-code": complete.PredictAnything,
"-province": complete.PredictAnything,
"-street-address": complete.PredictAnything,
})
}

Expand All @@ -97,10 +147,17 @@ func (c *TLSCACreateCommand) Run(args []string) int {
flagSet := c.Meta.FlagSet(c.Name(), FlagSetClient)
flagSet.Usage = func() { c.Ui.Output(c.Help()) }
flagSet.Var(&c.additionalDomain, "additional-domain", "")
flagSet.IntVar(&c.days, "days", 1825, "")
flagSet.IntVar(&c.days, "days", 0, "")
flagSet.BoolVar(&c.constraint, "name-constraint", false, "")
flagSet.StringVar(&c.domain, "domain", "nomad", "")
flagSet.StringVar(&c.domain, "domain", "", "")
flagSet.StringVar(&c.commonName, "common-name", "", "")
flagSet.StringVar(&c.country, "country", "", "")
flagSet.StringVar(&c.postalCode, "postal-code", "", "")
flagSet.StringVar(&c.province, "province", "", "")
flagSet.StringVar(&c.locality, "locality", "", "")
flagSet.StringVar(&c.streetAddress, "street-address", "", "")
flagSet.StringVar(&c.organization, "organization", "", "")
flagSet.StringVar(&c.organizationalUnit, "organizational-unit", "", "")
if err := flagSet.Parse(args); err != nil {
return 1
}
Expand All @@ -112,6 +169,32 @@ func (c *TLSCACreateCommand) Run(args []string) int {
c.Ui.Error(commandErrorText(c))
return 1
}
if c.IsCustom() && c.days != 0 || c.IsCustom() {
c.domain = "nomad"
} else {
if c.commonName == "" {
c.Ui.Error("Please provide the -common-name flag when customizing the CA")
c.Ui.Error(commandErrorText(c))
return 1
}
if c.country == "" {
c.Ui.Error("Please provide the -country flag when customizing the CA")
c.Ui.Error(commandErrorText(c))
return 1
}

if c.organization == "" {
c.Ui.Error("Please provide the -organization flag when customizing the CA")
c.Ui.Error(commandErrorText(c))
return 1
}

if c.organizationalUnit == "" {
c.Ui.Error("Please provide the -organizational-unit flag when customizing the CA")
c.Ui.Error(commandErrorText(c))
return 1
}
}
if c.domain != "" && c.domain != "nomad" && !c.constraint {
c.Ui.Error("Please provide the -name-constraint flag to use a custom domain constraint")
return 1
Expand Down Expand Up @@ -143,7 +226,18 @@ func (c *TLSCACreateCommand) Run(args []string) int {
constraints = append(constraints, c.additionalDomain...)
}

ca, pk, err := tlsutil.GenerateCA(tlsutil.CAOpts{Name: c.commonName, Days: c.days, Domain: c.domain, PermittedDNSDomains: constraints})
ca, pk, err := tlsutil.GenerateCA(tlsutil.CAOpts{
Name: c.commonName,
Days: c.days,
PermittedDNSDomains: constraints,
Country: c.country,
PostalCode: c.postalCode,
Province: c.province,
Locality: c.locality,
StreetAddress: c.streetAddress,
Organization: c.organization,
OrganizationalUnit: c.organizationalUnit,
})
if err != nil {
c.Ui.Error(err.Error())
return 1
Expand All @@ -163,3 +257,17 @@ func (c *TLSCACreateCommand) Run(args []string) int {

return 0
}

// IsCustom checks whether any of TLSCACreateCommand parameters have been populated with
// non-default values.
func (c *TLSCACreateCommand) IsCustom() bool {
return c.commonName == "" &&
c.country == "" &&
c.postalCode == "" &&
c.province == "" &&
c.locality == "" &&
c.streetAddress == "" &&
c.organization == "" &&
c.organizationalUnit == ""

}
24 changes: 11 additions & 13 deletions command/tls_ca_create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ package command
import (
"crypto/x509"
"os"
"strings"
"testing"
"time"

Expand Down Expand Up @@ -47,6 +46,10 @@ func TestCACreateCommand(t *testing.T) {
"-name-constraint=true",
"-domain=foo",
"-additional-domain=bar",
"-common-name=CustomCA",
"-country=ZZ",
"-organization=CustOrg",
"-organizational-unit=CustOrgUnit",
},
"foo-agent-ca.pem",
"foo-agent-ca-key.pem",
Expand All @@ -55,24 +58,20 @@ func TestCACreateCommand(t *testing.T) {
require.True(t, cert.PermittedDNSDomainsCritical)
require.Len(t, cert.PermittedDNSDomains, 4)
require.ElementsMatch(t, cert.PermittedDNSDomains, []string{"nomad", "foo", "localhost", "bar"})
require.Equal(t, cert.Issuer.Organization, []string{"CustOrg"})
require.Equal(t, cert.Issuer.OrganizationalUnit, []string{"CustOrgUnit"})
require.Equal(t, cert.Issuer.Country, []string{"ZZ"})
require.Contains(t, cert.Issuer.CommonName, "CustomCA")
},
},
{"with common-name",
{"ca custom date",
[]string{
"-common-name=foo",
},
"nomad-agent-ca.pem",
"nomad-agent-ca-key.pem",
func(t *testing.T, cert *x509.Certificate) {
require.Equal(t, cert.Subject.CommonName, "foo")
"-days=365",
},
},
{"without common-name",
[]string{},
"nomad-agent-ca.pem",
"nomad-agent-ca-key.pem",
func(t *testing.T, cert *x509.Certificate) {
require.True(t, strings.HasPrefix(cert.Subject.CommonName, "Nomad Agent CA"))
require.Equal(t, 365*24*time.Hour, time.Until(cert.NotAfter).Round(24*time.Hour))
},
},
}
Expand All @@ -97,5 +96,4 @@ func TestCACreateCommand(t *testing.T) {
require.NoError(t, os.Remove(tc.keyPath))
})
}

}
87 changes: 67 additions & 20 deletions helper/tlsutil/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"errors"
"fmt"
"math/big"
"net"
Expand Down Expand Up @@ -66,7 +67,13 @@ type CAOpts struct {
Serial *big.Int
Days int
PermittedDNSDomains []string
Domain string
Country string
PostalCode string
Province string
Locality string
StreetAddress string
Organization string
OrganizationalUnit string
Name string
}

Expand All @@ -81,10 +88,28 @@ type CertOpts struct {
ExtKeyUsage []x509.ExtKeyUsage
}

// IsCustom checks whether any of CAOpts parameters have been populated with
// non-default values.
func (c *CAOpts) IsCustom() bool {
return c.Country == "" &&
c.PostalCode == "" &&
c.Province == "" &&
c.Locality == "" &&
c.StreetAddress == "" &&
c.Organization == "" &&
c.OrganizationalUnit == "" &&
c.Name == ""
}

// GenerateCA generates a new CA for agent TLS (not to be confused with Connect TLS)
func GenerateCA(opts CAOpts) (string, string, error) {
signer := opts.Signer
var pk string
var (
id []byte
pk string
err error
signer = opts.Signer
sn = opts.Serial
)
if signer == nil {
var err error
signer, pk, err = GeneratePrivateKey()
Expand All @@ -93,45 +118,67 @@ func GenerateCA(opts CAOpts) (string, string, error) {
}
}

id, err := keyID(signer.Public())
id, err = keyID(signer.Public())
if err != nil {
return "", "", err
}

sn := opts.Serial
if sn == nil {
var err error
sn, err = GenerateSerialNumber()
if err != nil {
return "", "", err
}
}
name := opts.Name
if name == "" {
name = fmt.Sprintf("Nomad Agent CA %d", sn)
}

days := opts.Days
if opts.Days == 0 {
days = 365
if opts.IsCustom() {
opts.Name = fmt.Sprintf("Nomad Agent CA %d", sn)
if opts.Days == 0 {
opts.Days = 1825
}
opts.Country = "US"
opts.PostalCode = "94105"
opts.Province = "CA"
opts.Locality = "San Francisco"
opts.StreetAddress = "101 Second Street"
opts.Organization = "HashiCorp Inc."
opts.OrganizationalUnit = "Nomad"
} else {
if opts.Name == "" {
return "", "", errors.New("common name value not provided")
} else {
opts.Name = fmt.Sprintf("%s %d", opts.Name, sn)
}
if opts.Country == "" {
return "", "", errors.New("country value not provided")
}

if opts.Organization == "" {
return "", "", errors.New("organization value not provided")
}

if opts.OrganizationalUnit == "" {
return "", "", errors.New("organizational unit value not provided")
}
}

// Create the CA cert
template := x509.Certificate{
SerialNumber: sn,
Subject: pkix.Name{
Country: []string{"US"},
PostalCode: []string{"94105"},
Province: []string{"CA"},
Locality: []string{"San Francisco"},
StreetAddress: []string{"101 Second Street"},
Organization: []string{"HashiCorp Inc."},
CommonName: name,
Country: []string{opts.Country},
PostalCode: []string{opts.PostalCode},
Province: []string{opts.Province},
Locality: []string{opts.Locality},
StreetAddress: []string{opts.StreetAddress},
Organization: []string{opts.Organization},
OrganizationalUnit: []string{opts.OrganizationalUnit},
CommonName: opts.Name,
},
BasicConstraintsValid: true,
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign | x509.KeyUsageDigitalSignature,
IsCA: true,
NotAfter: time.Now().AddDate(0, 0, days),
NotAfter: time.Now().AddDate(0, 0, opts.Days),
NotBefore: time.Now(),
AuthorityKeyId: id,
SubjectKeyId: id,
Expand Down
Loading

0 comments on commit 1541358

Please sign in to comment.