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
}