Add IP Address in CSR

Signed-off-by: Laurent Rolaz <laurent.rolaz@gmail.com>
This commit is contained in:
Laurent Rolaz 2018-11-30 19:15:54 +01:00 committed by Laurent ROLAZ
parent 670cd8564f
commit 6dcc408741
11 changed files with 108 additions and 6 deletions

2
.gitignore vendored
View File

@ -7,3 +7,5 @@
.vscode
.venv
bazel-*
/.settings/
/.project

View File

@ -18,6 +18,7 @@ package v1alpha1
const (
AltNamesAnnotationKey = "certmanager.k8s.io/alt-names"
IpSansAnnotationKey = "certmanager.k8s.io/ip-sans"
CommonNameAnnotationKey = "certmanager.k8s.io/common-name"
IssuerNameAnnotationKey = "certmanager.k8s.io/issuer-name"
IssuerKindAnnotationKey = "certmanager.k8s.io/issuer-kind"

View File

@ -66,6 +66,9 @@ type CertificateSpec struct {
// DNSNames is a list of subject alt names to be used on the Certificate
DNSNames []string `json:"dnsNames,omitempty"`
// IPAddresses is a list of IP addresses to be used on the Certificate
IPAddresses []string `json:"ipAddresses,omitempty"`
// SecretName is the name of the secret resource to store this secret in
SecretName string `json:"secretName"`

View File

@ -491,6 +491,11 @@ func (in *CertificateSpec) DeepCopyInto(out *CertificateSpec) {
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.IPAddresses != nil {
in, out := &in.IPAddresses, &out.IPAddresses
*out = make([]string, len(*in))
copy(*out, *in)
}
out.IssuerRef = in.IssuerRef
if in.ACME != nil {
in, out := &in.ACME, &out.ACME

View File

@ -18,6 +18,7 @@ package validation
import (
"fmt"
"net"
"k8s.io/apimachinery/pkg/util/validation/field"
@ -49,6 +50,9 @@ func ValidateCertificateSpec(crt *v1alpha1.CertificateSpec, fldPath *field.Path)
if len(crt.CommonName) == 0 && len(crt.DNSNames) == 0 {
el = append(el, field.Required(fldPath.Child("dnsNames"), "at least one dnsName is required if commonName is not set"))
}
if len(crt.IPAddresses) > 0 {
el = append(el, validateIPAddresses(crt, fldPath)...)
}
if crt.ACME != nil {
el = append(el, validateACMEConfigForAllDNSNames(crt, fldPath)...)
el = append(el, ValidateACMECertificateConfig(crt.ACME, fldPath.Child("acme"))...)
@ -104,6 +108,20 @@ func validateACMEConfigForAllDNSNames(a *v1alpha1.CertificateSpec, fldPath *fiel
return el
}
func validateIPAddresses(a *v1alpha1.CertificateSpec, fldPath *field.Path) field.ErrorList {
if len(a.IPAddresses) <= 0 {
return nil
}
el := field.ErrorList{}
for i, d := range a.IPAddresses {
ip := net.ParseIP(d)
if ip == nil {
el = append(el, field.Invalid(fldPath.Child("ipAddresses").Index(i), d, "Invalid IP Address"))
}
}
return el
}
func ValidateACMECertificateConfig(a *v1alpha1.ACMECertificateConfig, fldPath *field.Path) field.ErrorList {
el := field.ErrorList{}
for i, cfg := range a.Config {

View File

@ -372,6 +372,29 @@ func TestValidateCertificate(t *testing.T) {
field.Invalid(fldPath.Child("keyAlgorithm"), v1alpha1.KeyAlgorithm("blah"), "must be either empty or one of rsa or ecdsa"),
},
},
"valid certificate with ipAddresses": {
cfg: &v1alpha1.Certificate{
Spec: v1alpha1.CertificateSpec{
CommonName: "testcn",
IPAddresses: []string{"127.0.0.1"},
SecretName: "abc",
IssuerRef: validIssuerRef,
},
},
},
"certificate with invalid ipAddresses": {
cfg: &v1alpha1.Certificate{
Spec: v1alpha1.CertificateSpec{
CommonName: "testcn",
IPAddresses: []string{"blah"},
SecretName: "abc",
IssuerRef: validIssuerRef,
},
},
errs: []*field.Error{
field.Invalid(fldPath.Child("ipAddresses").Index(0), "blah", "Invalid IP Address"),
},
},
}
for n, s := range scenarios {
t.Run(n, func(t *testing.T) {

View File

@ -216,6 +216,12 @@ func (c *Controller) certificateMatchesSpec(crt *v1alpha1.Certificate, key crypt
errs = append(errs, fmt.Sprintf("DNS names on TLS certificate not up to date: %q", cert.DNSNames))
}
// validate the ip addresses are correct
expectedIPAddresses := pki.IPAddressesNameForCertificate(crt)
if !util.EqualUnsorted(util.IPAddressesToString(cert.IPAddresses), expectedIPAddresses) {
errs = append(errs, fmt.Sprintf("IP Addresses on TLS certificate not up to date: %q", util.IPAddressesToString(cert.IPAddresses)))
}
return len(errs) == 0, errs
}
@ -314,6 +320,7 @@ func (c *Controller) updateSecret(crt *v1alpha1.Certificate, namespace string, c
secret.Annotations[v1alpha1.IssuerKindAnnotationKey] = issuerKind(crt)
secret.Annotations[v1alpha1.CommonNameAnnotationKey] = x509Cert.Subject.CommonName
secret.Annotations[v1alpha1.AltNamesAnnotationKey] = strings.Join(x509Cert.DNSNames, ",")
secret.Annotations[v1alpha1.IpSansAnnotationKey] = strings.Join(util.IPAddressesToString(x509Cert.IPAddresses), ",")
}
// Always set the certificate name label on the target secret

View File

@ -57,6 +57,7 @@ var serialNumberLimit = new(big.Int).Lsh(big.NewInt(1), 128)
func generateSelfSignedCert(t *testing.T, crt *v1alpha1.Certificate, key crypto.Signer, duration time.Duration) (derBytes, pemBytes []byte) {
commonName := pki.CommonNameForCertificate(crt)
dnsNames := pki.DNSNamesForCertificate(crt)
ipAddresses := pki.IPAddressesForCertificate(crt)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
@ -76,6 +77,7 @@ func generateSelfSignedCert(t *testing.T, crt *v1alpha1.Certificate, key crypto.
// see http://golang.org/pkg/crypto/x509/#KeyUsage
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
DNSNames: dnsNames,
IPAddresses: ipAddresses,
}
derBytes, err = x509.CreateCertificate(rand.Reader, template, template, key.Public(), key)

View File

@ -26,6 +26,7 @@ import (
"path"
"strings"
"time"
"net"
"github.com/golang/glog"
vault "github.com/hashicorp/vault/api"
@ -93,7 +94,7 @@ func (v *Vault) Issue(ctx context.Context, crt *v1alpha1.Certificate) (*issuer.I
certDuration = crt.Spec.Duration.Duration
}
certPem, caPem, err := v.requestVaultCert(template.Subject.CommonName, certDuration, template.DNSNames, pemRequestBuf.Bytes())
certPem, caPem, err := v.requestVaultCert(template.Subject.CommonName, certDuration, template.DNSNames, ipAddressesToString(template.IPAddresses), pemRequestBuf.Bytes())
if err != nil {
v.Recorder.Eventf(crt, corev1.EventTypeWarning, "ErrorSigning", "Failed to request certificate: %v", err)
return nil, err
@ -214,17 +215,19 @@ func (v *Vault) requestTokenWithAppRoleRef(client *vault.Client, appRole *v1alph
return token, nil
}
func (v *Vault) requestVaultCert(commonName string, certDuration time.Duration, altNames []string, csr []byte) ([]byte, []byte, error) {
func (v *Vault) requestVaultCert(commonName string, certDuration time.Duration, altNames []string, ipSans []string, csr []byte) ([]byte, []byte, error) {
client, err := v.initVaultClient()
if err != nil {
return nil, nil, err
}
glog.V(4).Infof("Vault certificate request for commonName %s altNames: %q", commonName, altNames)
glog.V(4).Infof("Vault certificate request for commonName %s altNames: %q ipSans: %q", commonName, altNames, ipSans)
parameters := map[string]string{
"common_name": commonName,
"alt_names": strings.Join(altNames, ","),
"ip_sans": strings.Join(ipSans, ","),
"ttl": certDuration.String(),
"csr": string(csr),
"exclude_cn_from_sans": "true",
@ -314,3 +317,11 @@ func (v *Vault) vaultTokenRef(name, key string) (string, error) {
return token, nil
}
func ipAddressesToString(ipAddresses []net.IP) []string {
var ipNames []string
for _, ip := range ipAddresses {
ipNames = append(ipNames, ip.String())
}
return ipNames
}

View File

@ -25,6 +25,7 @@ import (
"encoding/pem"
"fmt"
"math/big"
"net"
"time"
"github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1"
@ -57,6 +58,22 @@ func DNSNamesForCertificate(crt *v1alpha1.Certificate) []string {
return crt.Spec.DNSNames
}
func IPAddressesForCertificate(crt *v1alpha1.Certificate) []net.IP {
var ipAddresses []net.IP
for _, ip := range IPAddressesNameForCertificate(crt) {
ipAddresses = append(ipAddresses, net.ParseIP(ip))
}
return ipAddresses
}
func IPAddressesNameForCertificate(crt *v1alpha1.Certificate) []string {
var ipAddressNames []string
for _, ip := range crt.Spec.IPAddresses {
ipAddressNames = append(ipAddressNames, ip)
}
return removeDuplicates(ipAddressNames)
}
func removeDuplicates(in []string) []string {
var found []string
Outer:
@ -93,6 +110,7 @@ var serialNumberLimit = new(big.Int).Lsh(big.NewInt(1), 128)
func GenerateCSR(issuer v1alpha1.GenericIssuer, crt *v1alpha1.Certificate) (*x509.CertificateRequest, error) {
commonName := CommonNameForCertificate(crt)
dnsNames := DNSNamesForCertificate(crt)
iPAddresses := IPAddressesForCertificate(crt)
organization := OrganizationForCertificate(crt)
if len(commonName) == 0 && len(dnsNames) == 0 {
@ -112,7 +130,8 @@ func GenerateCSR(issuer v1alpha1.GenericIssuer, crt *v1alpha1.Certificate) (*x50
Organization: organization,
CommonName: commonName,
},
DNSNames: dnsNames,
DNSNames: dnsNames,
IPAddresses: iPAddresses,
// TODO: work out how best to handle extensions/key usages here
ExtraExtensions: []pkix.Extension{},
}, nil
@ -125,6 +144,7 @@ func GenerateCSR(issuer v1alpha1.GenericIssuer, crt *v1alpha1.Certificate) (*x50
func GenerateTemplate(issuer v1alpha1.GenericIssuer, crt *v1alpha1.Certificate) (*x509.Certificate, error) {
commonName := CommonNameForCertificate(crt)
dnsNames := DNSNamesForCertificate(crt)
iPAddresses := IPAddressesForCertificate(crt)
organization := OrganizationForCertificate(crt)
if len(commonName) == 0 && len(dnsNames) == 0 {
@ -164,8 +184,9 @@ func GenerateTemplate(issuer v1alpha1.GenericIssuer, crt *v1alpha1.Certificate)
NotBefore: time.Now(),
NotAfter: time.Now().Add(certDuration),
// see http://golang.org/pkg/crypto/x509/#KeyUsage
KeyUsage: keyUsages,
DNSNames: dnsNames,
KeyUsage: keyUsages,
DNSNames: dnsNames,
IPAddresses: iPAddresses,
}, nil
}

View File

@ -20,6 +20,7 @@ import (
"math/rand"
"sort"
"time"
"net"
)
func OnlyOneNotNil(items ...interface{}) (any bool, one bool) {
@ -75,3 +76,11 @@ func Contains(ss []string, s string) bool {
}
return false
}
func IPAddressesToString(ipAddresses []net.IP) []string {
var ipNames []string
for _, ip := range ipAddresses {
ipNames = append(ipNames, ip.String())
}
return ipNames
}