diff --git a/.gitignore b/.gitignore index 90ac3d2f0..a264827f4 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ .vscode .venv bazel-* +/.settings/ +/.project diff --git a/docs/generated/reference/output/reference/api-docs/index.html b/docs/generated/reference/output/reference/api-docs/index.html index 3ff5b358b..a8b1c08ac 100755 --- a/docs/generated/reference/output/reference/api-docs/index.html +++ b/docs/generated/reference/output/reference/api-docs/index.html @@ -101,6 +101,10 @@ Appears In: Certificate default Duration +ipAddresses
string array +IPAddresses is a list of IP addresses to be used on the Certificate + + isCA
boolean IsCA will mark this Certificate as valid for signing. This implies that the 'signing' usage is set diff --git a/pkg/apis/certmanager/v1alpha1/types.go b/pkg/apis/certmanager/v1alpha1/types.go index 3bd1cf29d..4c4bed1b6 100644 --- a/pkg/apis/certmanager/v1alpha1/types.go +++ b/pkg/apis/certmanager/v1alpha1/types.go @@ -18,6 +18,7 @@ package v1alpha1 const ( AltNamesAnnotationKey = "certmanager.k8s.io/alt-names" + IPSANAnnotationKey = "certmanager.k8s.io/ip-sans" CommonNameAnnotationKey = "certmanager.k8s.io/common-name" IssuerNameAnnotationKey = "certmanager.k8s.io/issuer-name" IssuerKindAnnotationKey = "certmanager.k8s.io/issuer-kind" diff --git a/pkg/apis/certmanager/v1alpha1/types_certificate.go b/pkg/apis/certmanager/v1alpha1/types_certificate.go index 12372229f..8c366de67 100644 --- a/pkg/apis/certmanager/v1alpha1/types_certificate.go +++ b/pkg/apis/certmanager/v1alpha1/types_certificate.go @@ -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"` diff --git a/pkg/apis/certmanager/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/certmanager/v1alpha1/zz_generated.deepcopy.go index 2aabc241e..47f38c64a 100644 --- a/pkg/apis/certmanager/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/certmanager/v1alpha1/zz_generated.deepcopy.go @@ -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 diff --git a/pkg/apis/certmanager/validation/certificate.go b/pkg/apis/certmanager/validation/certificate.go index ced43cb1f..02d5bcf9a 100644 --- a/pkg/apis/certmanager/validation/certificate.go +++ b/pkg/apis/certmanager/validation/certificate.go @@ -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 { diff --git a/pkg/apis/certmanager/validation/certificate_for_issuer.go b/pkg/apis/certmanager/validation/certificate_for_issuer.go index f80091d26..4ae90c469 100644 --- a/pkg/apis/certmanager/validation/certificate_for_issuer.go +++ b/pkg/apis/certmanager/validation/certificate_for_issuer.go @@ -63,6 +63,10 @@ func ValidateCertificateForACMEIssuer(crt *v1alpha1.CertificateSpec, issuer *v1a el = append(el, field.Invalid(specPath.Child("duration"), crt.Duration, "ACME does not support certificate durations")) } + if len(crt.IPAddresses) != 0 { + el = append(el, field.Invalid(specPath.Child("ipAddresses"), crt.IPAddresses, "ACME does not support certificate ip addresses")) + } + return el } diff --git a/pkg/apis/certmanager/validation/certificate_for_issuer_test.go b/pkg/apis/certmanager/validation/certificate_for_issuer_test.go index 0f81e7d84..a942c5456 100644 --- a/pkg/apis/certmanager/validation/certificate_for_issuer_test.go +++ b/pkg/apis/certmanager/validation/certificate_for_issuer_test.go @@ -158,6 +158,31 @@ func TestValidateCertificateForIssuer(t *testing.T) { field.Invalid(fldPath.Child("duration"), &metav1.Duration{Duration: time.Minute * 60}, "ACME does not support certificate durations"), }, }, + "acme certificate with ipAddresses set": { + crt: &v1alpha1.Certificate{ + Spec: v1alpha1.CertificateSpec{ + IPAddresses: []string{"127.0.0.1"}, + IssuerRef: validIssuerRef, + ACME: &v1alpha1.ACMECertificateConfig{ + Config: []v1alpha1.DomainSolverConfig{ + { + Domains: []string{"example.com"}, + SolverConfig: v1alpha1.SolverConfig{ + HTTP01: &v1alpha1.HTTP01SolverConfig{}, + }, + }, + }, + }, + }, + }, + issuer: generate.Issuer(generate.IssuerConfig{ + Name: defaultTestIssuerName, + Namespace: defaultTestNamespace, + }), + errs: []*field.Error{ + field.Invalid(fldPath.Child("ipAddresses"), []string{"127.0.0.1"}, "ACME does not support certificate ip addresses"), + }, + }, "acme certificate with renewBefore set": { crt: &v1alpha1.Certificate{ Spec: v1alpha1.CertificateSpec{ diff --git a/pkg/apis/certmanager/validation/certificate_test.go b/pkg/apis/certmanager/validation/certificate_test.go index 5f3a4a2be..ad957c8e0 100644 --- a/pkg/apis/certmanager/validation/certificate_test.go +++ b/pkg/apis/certmanager/validation/certificate_test.go @@ -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) { diff --git a/pkg/controller/certificates/sync.go b/pkg/controller/certificates/sync.go index 12c2f7443..78b84a261 100644 --- a/pkg/controller/certificates/sync.go +++ b/pkg/controller/certificates/sync.go @@ -221,6 +221,11 @@ 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 + if !util.EqualUnsorted(pki.IPAddressesToString(cert.IPAddresses), crt.Spec.IPAddresses) { + errs = append(errs, fmt.Sprintf("IP addresses on TLS certificate not up to date: %q", pki.IPAddressesToString(cert.IPAddresses))) + } + return len(errs) == 0, errs } @@ -322,6 +327,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.IPSANAnnotationKey] = strings.Join(pki.IPAddressesToString(x509Cert.IPAddresses), ",") } // Always set the certificate name label on the target secret diff --git a/pkg/issuer/acme/issue_test.go b/pkg/issuer/acme/issue_test.go index 1421ae99e..a8149667f 100644 --- a/pkg/issuer/acme/issue_test.go +++ b/pkg/issuer/acme/issue_test.go @@ -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 { @@ -74,8 +75,9 @@ func generateSelfSignedCert(t *testing.T, crt *v1alpha1.Certificate, key crypto. NotBefore: time.Now(), NotAfter: time.Now().Add(duration), // see http://golang.org/pkg/crypto/x509/#KeyUsage - KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, - DNSNames: dnsNames, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, + DNSNames: dnsNames, + IPAddresses: ipAddresses, } derBytes, err = x509.CreateCertificate(rand.Reader, template, template, key.Public(), key) diff --git a/pkg/issuer/vault/issue.go b/pkg/issuer/vault/issue.go index 817fec95b..62ebbb95d 100644 --- a/pkg/issuer/vault/issue.go +++ b/pkg/issuer/vault/issue.go @@ -93,7 +93,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, pki.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 +214,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", diff --git a/pkg/util/pki/csr.go b/pkg/util/pki/csr.go index 8a7597342..cfa1a4b10 100644 --- a/pkg/util/pki/csr.go +++ b/pkg/util/pki/csr.go @@ -25,6 +25,7 @@ import ( "encoding/pem" "fmt" "math/big" + "net" "time" "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1" @@ -57,6 +58,26 @@ func DNSNamesForCertificate(crt *v1alpha1.Certificate) []string { return crt.Spec.DNSNames } +func IPAddressesForCertificate(crt *v1alpha1.Certificate) []net.IP { + var ipAddresses []net.IP + var ip net.IP + for _, ipName := range crt.Spec.IPAddresses { + ip = net.ParseIP(ipName) + if ip != nil { + ipAddresses = append(ipAddresses, ip) + } + } + return ipAddresses +} + +func IPAddressesToString(ipAddresses []net.IP) []string { + var ipNames []string + for _, ip := range ipAddresses { + ipNames = append(ipNames, ip.String()) + } + return ipNames +} + func removeDuplicates(in []string) []string { var found []string Outer: @@ -93,6 +114,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 +134,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 +148,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 +188,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 }