Add Venafi Issuer implementation
Signed-off-by: James Munnelly <james@munnelly.eu>
This commit is contained in:
parent
c3f696fa1a
commit
cb7dbc830c
@ -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"],
|
||||
|
||||
61
pkg/issuer/venafi/BUILD.bazel
Normal file
61
pkg/issuer/venafi/BUILD.bazel
Normal file
@ -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",
|
||||
],
|
||||
)
|
||||
75
pkg/issuer/venafi/connector_test.go
Normal file
75
pkg/issuer/venafi/connector_test.go
Normal file
@ -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)
|
||||
}
|
||||
107
pkg/issuer/venafi/fixture_test.go
Normal file
107
pkg/issuer/venafi/fixture_test.go
Normal file
@ -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
|
||||
}
|
||||
193
pkg/issuer/venafi/issue.go
Normal file
193
pkg/issuer/venafi/issue.go
Normal file
@ -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
|
||||
}
|
||||
173
pkg/issuer/venafi/issue_test.go
Normal file
173
pkg/issuer/venafi/issue_test.go
Normal file
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
52
pkg/issuer/venafi/setup.go
Normal file
52
pkg/issuer/venafi/setup.go
Normal file
@ -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
|
||||
}
|
||||
151
pkg/issuer/venafi/venafi.go
Normal file
151
pkg/issuer/venafi/venafi.go
Normal file
@ -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)
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user