diff --git a/pkg/issuer/BUILD.bazel b/pkg/issuer/BUILD.bazel index dc0a1d34c..dc3da0acf 100644 --- a/pkg/issuer/BUILD.bazel +++ b/pkg/issuer/BUILD.bazel @@ -33,6 +33,7 @@ filegroup( "//pkg/issuer/fake:all-srcs", "//pkg/issuer/selfsigned:all-srcs", "//pkg/issuer/vault:all-srcs", + "//pkg/issuer/venafi:all-srcs", ], tags = ["automanaged"], visibility = ["//visibility:public"], diff --git a/pkg/issuer/venafi/BUILD.bazel b/pkg/issuer/venafi/BUILD.bazel new file mode 100644 index 000000000..85dcdaaca --- /dev/null +++ b/pkg/issuer/venafi/BUILD.bazel @@ -0,0 +1,61 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = [ + "issue.go", + "setup.go", + "venafi.go", + ], + importpath = "github.com/jetstack/cert-manager/pkg/issuer/venafi", + visibility = ["//visibility:public"], + deps = [ + "//pkg/api/util:go_default_library", + "//pkg/apis/certmanager/v1alpha1:go_default_library", + "//pkg/controller:go_default_library", + "//pkg/issuer:go_default_library", + "//pkg/util/pki:go_default_library", + "//vendor/github.com/Venafi/vcert:go_default_library", + "//vendor/github.com/Venafi/vcert/pkg/certificate:go_default_library", + "//vendor/github.com/Venafi/vcert/pkg/endpoint:go_default_library", + "//vendor/k8s.io/api/core/v1:go_default_library", + "//vendor/k8s.io/client-go/listers/core/v1:go_default_library", + "//vendor/k8s.io/klog:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) + +go_test( + name = "go_default_test", + srcs = [ + "connector_test.go", + "fixture_test.go", + "issue_test.go", + ], + embed = [":go_default_library"], + deps = [ + "//pkg/apis/certmanager/v1alpha1:go_default_library", + "//pkg/controller/test:go_default_library", + "//pkg/issuer:go_default_library", + "//pkg/util:go_default_library", + "//pkg/util/pki:go_default_library", + "//test/unit/gen:go_default_library", + "//vendor/github.com/Venafi/vcert/pkg/certificate:go_default_library", + "//vendor/github.com/Venafi/vcert/pkg/endpoint:go_default_library", + "//vendor/github.com/Venafi/vcert/pkg/venafi/fake:go_default_library", + "//vendor/github.com/kr/pretty:go_default_library", + ], +) diff --git a/pkg/issuer/venafi/connector_test.go b/pkg/issuer/venafi/connector_test.go new file mode 100644 index 000000000..e7cf00b44 --- /dev/null +++ b/pkg/issuer/venafi/connector_test.go @@ -0,0 +1,75 @@ +/* +Copyright 2018 The Jetstack cert-manager contributors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package venafi + +import ( + "github.com/Venafi/vcert/pkg/certificate" + "github.com/Venafi/vcert/pkg/endpoint" + "github.com/Venafi/vcert/pkg/venafi/fake" +) + +type fakeConnector struct { + *fake.Connector + + PingFunc func() error + ReadZoneConfigurationFunc func(string) (*endpoint.ZoneConfiguration, error) + RetrieveCertificateFunc func(*certificate.Request) (*certificate.PEMCollection, error) + RequestCertificateFunc func(*certificate.Request, string) (string, error) + RenewCertificateFunc func(*certificate.RenewalRequest) (string, error) +} + +func (f fakeConnector) Default() *fakeConnector { + if f.Connector == nil { + f.Connector = fake.NewConnector(true, nil) + } + return &f +} + +func (f *fakeConnector) Ping() (err error) { + if f.PingFunc != nil { + return f.PingFunc() + } + return f.Connector.Ping() +} + +func (f *fakeConnector) ReadZoneConfiguration(zone string) (config *endpoint.ZoneConfiguration, err error) { + if f.ReadZoneConfigurationFunc != nil { + return f.ReadZoneConfigurationFunc(zone) + } + return f.Connector.ReadZoneConfiguration(zone) +} + +func (f *fakeConnector) RetrieveCertificate(req *certificate.Request) (certificates *certificate.PEMCollection, err error) { + if f.RetrieveCertificateFunc != nil { + return f.RetrieveCertificateFunc(req) + } + return f.Connector.RetrieveCertificate(req) +} + +func (f *fakeConnector) RequestCertificate(req *certificate.Request, zone string) (requestID string, err error) { + if f.RequestCertificateFunc != nil { + return f.RequestCertificateFunc(req, zone) + } + return f.Connector.RequestCertificate(req, zone) +} + +func (f *fakeConnector) RenewCertificate(req *certificate.RenewalRequest) (requestID string, err error) { + if f.RenewCertificateFunc != nil { + return f.RenewCertificateFunc(req) + } + return f.Connector.RenewCertificate(req) +} diff --git a/pkg/issuer/venafi/fixture_test.go b/pkg/issuer/venafi/fixture_test.go new file mode 100644 index 000000000..3d1bc3ba6 --- /dev/null +++ b/pkg/issuer/venafi/fixture_test.go @@ -0,0 +1,107 @@ +/* +Copyright 2018 The Jetstack cert-manager contributors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package venafi + +import ( + "context" + "testing" + + vfake "github.com/Venafi/vcert/pkg/venafi/fake" + + "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1" + "github.com/jetstack/cert-manager/pkg/controller/test" +) + +type fixture struct { + Venafi *Venafi + *test.Builder + + Issuer v1alpha1.GenericIssuer + Certificate *v1alpha1.Certificate + Client connector + + PreFn func(*testing.T, *fixture) + CheckFn func(*testing.T, *fixture, ...interface{}) + Err bool + + Ctx context.Context +} + +func (s *fixture) Setup(t *testing.T) { + if s.Issuer == nil { + s.Issuer = &v1alpha1.Issuer{ + Spec: v1alpha1.IssuerSpec{ + IssuerConfig: v1alpha1.IssuerConfig{ + Venafi: &v1alpha1.VenafiIssuer{}, + }, + }, + } + } + // if a custom client has not been provided, we will use the vcert fake + // which generates certificates and private keys by default + if s.Client == nil { + s.Client = vfake.NewConnector(true, nil) + } + if s.Ctx == nil { + s.Ctx = context.Background() + } + if s.Builder == nil { + // TODO: set default IssuerOptions + // defaultTestAcmeClusterResourceNamespace, + // defaultTestSolverImage, + // default dns01 nameservers + // ambient credentials settings + s.Builder = &test.Builder{} + } + s.Venafi = s.buildFakeVenafi(s.Builder, s.Issuer) + if s.PreFn != nil { + s.PreFn(t, s) + s.Builder.Sync() + } +} + +func (s *fixture) Finish(t *testing.T, args ...interface{}) { + defer s.Builder.Stop() + if err := s.Builder.AllReactorsCalled(); err != nil { + t.Errorf("Not all expected reactors were called: %v", err) + } + if err := s.Builder.AllActionsExecuted(); err != nil { + t.Errorf(err.Error()) + } + + // resync listers before running checks + s.Builder.Sync() + // run custom checks + if s.CheckFn != nil { + s.CheckFn(t, s, args...) + } +} + +func (s *fixture) buildFakeVenafi(b *test.Builder, issuer v1alpha1.GenericIssuer) *Venafi { + b.Start() + // TODO: replace this with a call to NewVenafi by somehow modifying it to allow + // injecting the fake venafi client. + v := &Venafi{ + issuer: issuer, + Context: s.Context, + resourceNamespace: s.Context.IssuerOptions.ResourceNamespace(issuer), + secretsLister: s.Context.KubeSharedInformerFactory.Core().V1().Secrets().Lister(), + client: s.Client, + } + b.Sync() + return v +} diff --git a/pkg/issuer/venafi/issue.go b/pkg/issuer/venafi/issue.go new file mode 100644 index 000000000..56ca579e5 --- /dev/null +++ b/pkg/issuer/venafi/issue.go @@ -0,0 +1,193 @@ +/* +Copyright 2018 The Jetstack cert-manager contributors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package venafi + +import ( + "context" + "crypto/x509" + "encoding/pem" + "fmt" + "strings" + "time" + + "github.com/Venafi/vcert/pkg/certificate" + "github.com/Venafi/vcert/pkg/endpoint" + corev1 "k8s.io/api/core/v1" + "k8s.io/klog" + + "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1" + "github.com/jetstack/cert-manager/pkg/issuer" + "github.com/jetstack/cert-manager/pkg/util/pki" +) + +const ( + reasonErrorPrivateKey = "PrivateKey" +) + +// Issue will attempt to issue a new certificate from the Venafi Issuer. +// The control flow is as follows: +// - Attempt to retrieve the existing private key +// - If it does not exist, generate one +// - Generate a certificate template +// - Read the zone configuration from the Venafi server +// - Create a Venafi request based on the certificate template +// - Set defaults on the request based on the zone +// - Validate the request against the zone +// - Submit the request +// - Wait for the request to be fulfilled and the certificate to be available +func (v *Venafi) Issue(ctx context.Context, crt *v1alpha1.Certificate) (*issuer.IssueResponse, error) { + v.Recorder.Event(crt, corev1.EventTypeNormal, "Issuing", "Requesting new certificate...") + v.Recorder.Event(crt, corev1.EventTypeNormal, "GenerateKey", "Generating new private key") + + // Always generate a new private key, as some Venafi configurations mandate + // unique private keys per issuance. + signeeKey, err := pki.GeneratePrivateKeyForCertificate(crt) + if err != nil { + klog.Errorf("Error getting private key %q for certificate: %v", crt.Spec.SecretName, err) + v.Recorder.Eventf(crt, corev1.EventTypeWarning, "PrivateKeyError", "Error generating certificate private key: %v", err) + // don't trigger a retry. An error from this function implies some + // invalid input parameters, and retrying without updating the + // resource will not help. + return nil, nil + } + + // extract the public component of the key + signeePublicKey, err := pki.PublicKeyForPrivateKey(signeeKey) + if err != nil { + klog.Errorf("Error getting public key from private key: %v", err) + return nil, err + } + + // We build a x509.Certificate as the vcert library has support for converting + // this into its own internal Certificate Request type. + tmpl, err := pki.GenerateTemplate(crt) + if err != nil { + return nil, err + } + + // TODO: we need some way to detect fields are defaulted on the template, + // or otherwise move certificate/csr template defaulting into its own + // function within the PKI package. + // For now, we manually 'fix' the certificate template returned above + if len(crt.Spec.Organization) == 0 { + tmpl.Subject.Organization = []string{} + } + + // set the PublicKey field on the certificate template so it can be checked + // by the vcert library + tmpl.PublicKey = signeePublicKey + + // Retrieve a copy of the Venafi zone. + // This contains default values and policy control info that we can apply + // and check against locally. + zoneName := v.issuer.GetSpec().Venafi.Zone + zoneCfg, err := v.client.ReadZoneConfiguration(zoneName) + if err != nil { + v.Recorder.Eventf(crt, corev1.EventTypeWarning, "ReadZone", "Failed to read Venafi zone configuration: %v", err) + return nil, err + } + + //// Begin building Venafi certificate Request + + // Create a vcert Request structure + vreq := newVRequest(tmpl) + + // Apply default values from the Venafi zone + zoneCfg.UpdateCertificateRequest(vreq) + err = zoneCfg.ValidateCertificateRequest(vreq) + if err != nil { + // TODO: set a certificate status condition instead of firing an event + // in case this step is particularly chatty + v.Recorder.Eventf(crt, corev1.EventTypeWarning, "Validate", "Failed to validate certificate against Venafi zone: %v", err) + return nil, err + } + + // Generate the actual x509 CSR and set it on the vreq + err = certificate.GenerateRequest(vreq, signeeKey) + if err != nil { + v.Recorder.Eventf(crt, corev1.EventTypeWarning, "GenerateCSR", "Failed to generate a CSR for the certificate: %v", err) + return nil, err + } + + // certificate.GenerateRequest above sets the CSR field as der encoded bytes + // however, the library actually requires this field to be PEM encoded. + // We decode the DER bytes and run them through the PEM encoder and re-set the + // field before actually calling RequestCertificate. + // TODO: make this weird behaviour go away + vreq.CSR = pem.EncodeToMemory(certificate.GetCertificateRequestPEMBlock(vreq.CSR)) + + // We mark the request as having a user provided CSR, as we have manually + // generated it in the lines above. + // Setting this will prevent a new private key being generated by vcert. + vreq.CsrOrigin = certificate.UserProvidedCSR + // TODO: better set the timeout here. Right now, we'll block for this amount of time. + vreq.Timeout = time.Minute * 5 + + // Actually send a request to the Venafi server for a certificate. + requestID, err := v.client.RequestCertificate(vreq, zoneName) + if err != nil { + v.Recorder.Eventf(crt, corev1.EventTypeWarning, "Request", "Failed to request a certificate from Venafi: %v", err) + return nil, err + } + + // Set the PickupID so vcert does not have to look it up by the fingerprint + vreq.PickupID = requestID + + // TODO: we probably need to check the error response here, as the certificate + // may still be provisioning. + // If so, we may *also* want to consider storing the pickup ID somewhere too + // so we can attempt to retrieve the certificate on the next sync (i.e. wait + // for issuance asynchronously). + pemCollection, err := v.client.RetrieveCertificate(vreq) + + // Check some known error types + if err, ok := err.(endpoint.ErrCertificatePending); ok { + v.Recorder.Eventf(crt, corev1.EventTypeWarning, "Retrieve", "Failed to retrieve a certificate from Venafi, still pending: %v", err) + return nil, fmt.Errorf("Venafi certificate still pending: %v", err) + } + if err, ok := err.(endpoint.ErrRetrieveCertificateTimeout); ok { + v.Recorder.Eventf(crt, corev1.EventTypeWarning, "Retrieve", "Failed to retrieve a certificate from Venafi, timed out: %v", err) + return nil, fmt.Errorf("Timed out waiting for certificate: %v", err) + } + if err != nil { + v.Recorder.Eventf(crt, corev1.EventTypeWarning, "Retrieve", "Failed to retrieve a certificate from Venafi: %v", err) + return nil, err + } + + // Encode the private key ready to be saved + pk, err := pki.EncodePrivateKey(signeeKey) + if err != nil { + return nil, err + } + + // Construct the certificate chain and return the new keypair + cs := append([]string{pemCollection.Certificate}, pemCollection.Chain...) + chain := strings.Join(cs, "\n") + return &issuer.IssueResponse{ + PrivateKey: pk, + Certificate: []byte(chain), + // TODO: obtain CA certificate somehow + // CA: []byte{}, + }, nil +} + +func newVRequest(cert *x509.Certificate) *certificate.Request { + req := certificate.NewRequest(cert) + // overwrite entire Subject block + req.Subject = cert.Subject + return req +} diff --git a/pkg/issuer/venafi/issue_test.go b/pkg/issuer/venafi/issue_test.go new file mode 100644 index 000000000..af29fbd3a --- /dev/null +++ b/pkg/issuer/venafi/issue_test.go @@ -0,0 +1,173 @@ +/* +Copyright 2018 The Jetstack cert-manager contributors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package venafi + +import ( + "reflect" + "testing" + + "github.com/Venafi/vcert/pkg/endpoint" + + cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1" + testpkg "github.com/jetstack/cert-manager/pkg/controller/test" + "github.com/jetstack/cert-manager/pkg/issuer" + "github.com/jetstack/cert-manager/pkg/util" + "github.com/jetstack/cert-manager/pkg/util/pki" + "github.com/jetstack/cert-manager/test/unit/gen" + "github.com/kr/pretty" +) + +func checkCertificateIssued(t *testing.T, s *fixture, args ...interface{}) { + returnedCert := args[0].(*cmapi.Certificate) + resp := args[1].(*issuer.IssueResponse) + + if err, ok := args[2].(error); ok && err != nil { + t.Errorf("expected no error to be returned, but got: %v", err) + return + } + if !reflect.DeepEqual(returnedCert, s.Certificate) { + t.Errorf("output was not as expected: %s", pretty.Diff(returnedCert, s.Certificate)) + } + pk, err := pki.DecodePrivateKeyBytes(resp.PrivateKey) + if err != nil { + t.Errorf("unable to decode private key: %v", err) + return + } + crt, err := pki.DecodeX509CertificateBytes(resp.Certificate) + if err != nil { + t.Errorf("unable to decode x509 certificate: %v", err) + return + } + ok, err := pki.PublicKeyMatchesCertificate(pk.Public(), crt) + if err != nil { + t.Errorf("error checking private key: %v", err) + return + } + if !ok { + t.Errorf("private key does not match certificate") + } + // validate the common name is correct + expectedCN := pki.CommonNameForCertificate(s.Certificate) + if expectedCN != crt.Subject.CommonName { + t.Errorf("expected common name to be %q but it was %q", expectedCN, crt.Subject.CommonName) + } + + // validate the dns names are correct + expectedDNSNames := pki.DNSNamesForCertificate(s.Certificate) + if !util.EqualUnsorted(crt.DNSNames, expectedDNSNames) { + t.Errorf("expected dns names to be %q but it was %q", expectedDNSNames, crt.DNSNames) + } +} + +func TestIssue(t *testing.T) { + tests := map[string]fixture{ + "obtain a certificate with a single dnsname specified": { + Certificate: gen.Certificate("testcrt", + gen.SetCertificateDNSNames("example.com"), + ), + CheckFn: checkCertificateIssued, + Err: false, + }, + "obtain a certificate with the organization field locked by the venafi zone": { + Certificate: gen.Certificate("testcrt", + gen.SetCertificateDNSNames("example.com"), + ), + Client: fakeConnector{ + ReadZoneConfigurationFunc: func(zone string) (*endpoint.ZoneConfiguration, error) { + return &endpoint.ZoneConfiguration{ + Organization: "testing-org", + OrganizationLocked: true, + }, nil + }, + }.Default(), + CheckFn: func(t *testing.T, s *fixture, args ...interface{}) { + checkCertificateIssued(t, s, args...) + resp := args[1].(*issuer.IssueResponse) + x509Cert, err := pki.DecodeX509CertificateBytes(resp.Certificate) + if err != nil { + t.Errorf("could not decode x509 certificate bytes: %v", err) + } + if x509Cert.Subject.Organization[0] != "testing-org" { + t.Errorf("expected organization field to be 'testing-org' but got: %s", x509Cert.Subject.Organization[0]) + } + }, + Err: false, + }, + "obtain a certificate with the organization field defaulted by the venafi zone": { + Certificate: gen.Certificate("testcrt", + gen.SetCertificateDNSNames("example.com"), + ), + Client: fakeConnector{ + ReadZoneConfigurationFunc: func(zone string) (*endpoint.ZoneConfiguration, error) { + return &endpoint.ZoneConfiguration{ + Organization: "testing-org", + OrganizationLocked: false, + }, nil + }, + }.Default(), + CheckFn: func(t *testing.T, s *fixture, args ...interface{}) { + checkCertificateIssued(t, s, args...) + + resp := args[1].(*issuer.IssueResponse) + x509Cert, err := pki.DecodeX509CertificateBytes(resp.Certificate) + if err != nil { + t.Errorf("could not decode x509 certificate bytes: %v", err) + } + if x509Cert.Subject.Organization[0] != "testing-org" { + t.Errorf("expected organization field to be 'testing-org' but got: %s", x509Cert.Subject.Organization[0]) + } + }, + Err: false, + }, + "obtain a certificate with the organization field set by the certificate": { + Certificate: gen.Certificate("testcrt", + gen.SetCertificateDNSNames("example.com"), + gen.SetCertificateOrganization("testing-crt-org"), + ), + CheckFn: func(t *testing.T, s *fixture, args ...interface{}) { + checkCertificateIssued(t, s, args...) + resp := args[1].(*issuer.IssueResponse) + x509Cert, err := pki.DecodeX509CertificateBytes(resp.Certificate) + if err != nil { + t.Errorf("could not decode x509 certificate bytes: %v", err) + } + if x509Cert.Subject.Organization[0] != "testing-crt-org" { + t.Errorf("expected organization field to be 'testing-crt-org' but got: %s", x509Cert.Subject.Organization[0]) + } + }, + Err: false, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + if test.Builder == nil { + test.Builder = &testpkg.Builder{} + } + test.Setup(t) + certCopy := test.Certificate.DeepCopy() + resp, err := test.Venafi.Issue(test.Ctx, certCopy) + if err != nil && !test.Err { + t.Errorf("Expected function to not error, but got: %v", err) + } + if err == nil && test.Err { + t.Errorf("Expected function to get an error, but got: %v", err) + } + test.Finish(t, certCopy, resp, err) + }) + } +} diff --git a/pkg/issuer/venafi/setup.go b/pkg/issuer/venafi/setup.go new file mode 100644 index 000000000..305dd71ea --- /dev/null +++ b/pkg/issuer/venafi/setup.go @@ -0,0 +1,52 @@ +/* +Copyright 2018 The Jetstack cert-manager contributors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package venafi + +import ( + "context" + "fmt" + + corev1 "k8s.io/api/core/v1" + "k8s.io/klog" + + apiutil "github.com/jetstack/cert-manager/pkg/api/util" + "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1" +) + +func (v *Venafi) Setup(ctx context.Context) error { + err := v.client.Ping() + if err != nil { + klog.Infof("Issuer could not connect to endpoint with provided credentials. Issuer failed to connect to endpoint\n") + apiutil.SetIssuerCondition(v.issuer, v1alpha1.IssuerConditionReady, v1alpha1.ConditionFalse, + "ErrorPing", fmt.Sprintf("Failed to connect to Venafi endpoint")) + return fmt.Errorf("error verifying Venafi client: %s", err.Error()) + } + + // If it does not already have a 'ready' condition, we'll also log an event + // to make it really clear to users that this Issuer is ready. + if !apiutil.IssuerHasCondition(v.issuer, v1alpha1.IssuerCondition{ + Type: v1alpha1.IssuerConditionReady, + Status: v1alpha1.ConditionTrue, + }) { + v.Recorder.Eventf(v.issuer, corev1.EventTypeNormal, "Ready", "Verified issuer with Venafi server") + } + + klog.Info("Venafi issuer started") + apiutil.SetIssuerCondition(v.issuer, v1alpha1.IssuerConditionReady, v1alpha1.ConditionTrue, "Venafi issuer started", "Venafi issuer started") + + return nil +} diff --git a/pkg/issuer/venafi/venafi.go b/pkg/issuer/venafi/venafi.go new file mode 100644 index 000000000..b0761d738 --- /dev/null +++ b/pkg/issuer/venafi/venafi.go @@ -0,0 +1,151 @@ +/* +Copyright 2018 The Jetstack cert-manager contributors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package venafi + +import ( + "fmt" + + "github.com/Venafi/vcert" + "github.com/Venafi/vcert/pkg/certificate" + "github.com/Venafi/vcert/pkg/endpoint" + corev1 "k8s.io/api/core/v1" + corelisters "k8s.io/client-go/listers/core/v1" + + cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1" + "github.com/jetstack/cert-manager/pkg/controller" + "github.com/jetstack/cert-manager/pkg/issuer" +) + +const ( + tppUsernameKey = "username" + tppPasswordKey = "password" + + defaultAPIKeyKey = "api-key" +) + +// Venafi is a implementation of govcert library to manager certificates from TPP or Venafi Cloud +type Venafi struct { + issuer cmapi.GenericIssuer + *controller.Context + + // Namespace in which to read resources related to this Issuer from. + // For Issuers, this will be the namespace of the Issuer. + // For ClusterIssuers, this will be the cluster resource namespace. + resourceNamespace string + secretsLister corelisters.SecretLister + + client connector +} + +// connector exposes a subset of the vcert Connector interface to make stubbing +// out its functionality during tests easier. +type connector interface { + Ping() (err error) + ReadZoneConfiguration(zone string) (config *endpoint.ZoneConfiguration, err error) + RequestCertificate(req *certificate.Request, zone string) (requestID string, err error) + RetrieveCertificate(req *certificate.Request) (certificates *certificate.PEMCollection, err error) + RenewCertificate(req *certificate.RenewalRequest) (requestID string, err error) +} + +func NewVenafi(ctx *controller.Context, issuer cmapi.GenericIssuer) (issuer.Interface, error) { + secretsLister := ctx.KubeSharedInformerFactory.Core().V1().Secrets().Lister() + resourceNamespace := ctx.IssuerOptions.ResourceNamespace(issuer) + + cfg, err := configForIssuer(issuer, secretsLister, resourceNamespace) + if err != nil { + ctx.Recorder.Eventf(issuer, corev1.EventTypeWarning, "FailedInit", "Failed to initialise issuer: %v", err) + return nil, err + } + + client, err := vcert.NewClient(cfg) + if err != nil { + ctx.Recorder.Eventf(issuer, corev1.EventTypeWarning, "FailedInit", "Failed to create Venafi client: %v", err) + return nil, fmt.Errorf("error creating Venafi client: %s", err.Error()) + } + + return &Venafi{ + issuer: issuer, + Context: ctx, + resourceNamespace: resourceNamespace, + secretsLister: secretsLister, + client: client, + }, nil +} + +// configForIssuer will convert a cert-manager Venafi issuer into a vcert.Config +// that can be used to instantiate an API client. +func configForIssuer(iss cmapi.GenericIssuer, secretsLister corelisters.SecretLister, resourceNamespace string) (*vcert.Config, error) { + venCfg := iss.GetSpec().Venafi + switch { + case venCfg.TPP != nil: + tpp := venCfg.TPP + tppSecret, err := secretsLister.Secrets(resourceNamespace).Get(tpp.CredentialsRef.Name) + if err != nil { + return nil, fmt.Errorf("error loading TPP credentials: %v", err) + } + + username := tppSecret.Data[tppUsernameKey] + password := tppSecret.Data[tppPasswordKey] + + caBundle := "" + if len(tpp.CABundle) > 0 { + caBundle = string(tpp.CABundle) + } + + return &vcert.Config{ + ConnectorType: endpoint.ConnectorTypeTPP, + BaseUrl: tpp.URL, + Zone: venCfg.Zone, + LogVerbose: venCfg.Verbose, + ConnectionTrust: caBundle, + Credentials: &endpoint.Authentication{ + User: string(username), + Password: string(password), + }, + }, nil + + case venCfg.Cloud != nil: + cloud := venCfg.Cloud + cloudSecret, err := secretsLister.Secrets(resourceNamespace).Get(cloud.APIKeySecretRef.Name) + if err != nil { + return nil, fmt.Errorf("error loading TPP credentials: %v", err) + } + + k := defaultAPIKeyKey + if cloud.APIKeySecretRef.Key != "" { + k = cloud.APIKeySecretRef.Key + } + apiKey := cloudSecret.Data[k] + + return &vcert.Config{ + ConnectorType: endpoint.ConnectorTypeCloud, + BaseUrl: cloud.URL, + Zone: venCfg.Zone, + LogVerbose: venCfg.Verbose, + Credentials: &endpoint.Authentication{ + APIKey: string(apiKey), + }, + }, nil + + default: + return nil, fmt.Errorf("neither Venafi Cloud or TPP configuration found") + } +} + +func init() { + controller.RegisterIssuer(controller.IssuerVenafi, NewVenafi) +}