Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the ability to customise the details of the CA #17309

Merged
merged 8 commits into from
Jul 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
lhaig marked this conversation as resolved.
Show resolved Hide resolved
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