Add IP Address in CSR
Signed-off-by: Laurent Rolaz <laurent.rolaz@gmail.com>
This commit is contained in:
parent
670cd8564f
commit
6dcc408741
2
.gitignore
vendored
2
.gitignore
vendored
@ -7,3 +7,5 @@
|
||||
.vscode
|
||||
.venv
|
||||
bazel-*
|
||||
/.settings/
|
||||
/.project
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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"`
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user