Skip to content

Commit

Permalink
Add AuthMethod to Vault issuer
Browse files Browse the repository at this point in the history
AuthMethod replaces the Token field. It is used to
configure the token used for the Vault API requests.
ConstantToken is equivalent to the old Token field.
  • Loading branch information
johanbrandhorst committed Dec 22, 2019
1 parent 871145a commit 6a8c57c
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 13 deletions.
18 changes: 18 additions & 0 deletions issuers/vault/types.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,28 @@
package vault

import (
"context"
"strings"
"time"

"github.com/hashicorp/vault/api"
)

// AuthMethod defines the interface required to implement
// custom authentication against the Vault server.
type AuthMethod interface {
GetToken(context.Context, *api.Client) (string, error)
}

// ConstantToken implements AuthMethod with a constant token
type ConstantToken string

// GetToken returns the token
func (c ConstantToken) GetToken(context.Context, *api.Client) (string, error) {
return string(c), nil
}


// https://www.vaultproject.io/api/secret/pki/index.html#parameters-14
type csrOpts struct {
CSR string `json:"csr"`
Expand Down
31 changes: 25 additions & 6 deletions issuers/vault/vault.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,26 @@ import (
// Issuer implements the Issuer interface with a
// Hashicorp Vault PKI Secrets Engine backend.
//
// URL, Token and Role are required.
// URL, Role and AuthMethod are required.
type Issuer struct {
// URL is the URL of the Vault instance.
URL *url.URL
// Role is the Vault Role that should be used
// when issuing certificates.
Role string

// Token is the Vault secret token that should be used
// when issuing certificates.
//
// Deprecated: use AuthMethod instead.
Token string
// AuthMethod configures the method used for authenticating
// against the Vault server.
AuthMethod AuthMethod

// Mount is the name under which the PKI secrets engine
// is mounted. Defaults to `pki`
Mount string
// Role is the Vault Role that should be used
// when issuing certificates.
Role string
// TLSConfig allows configuration of the TLS config
// used when connecting to the Vault server.
TLSConfig *tls.Config
Expand Down Expand Up @@ -64,7 +71,7 @@ type Issuer struct {

// FromClient returns an Issuer using the provided Vault API client.
// Any changes to the issuers properties (such as setting the TTL or adding Other SANS)
// must be done before using it. The client must have its token configured.
// must be done before using it.
func FromClient(v *api.Client, role string) *Issuer {
return &Issuer{
Role: role,
Expand All @@ -86,7 +93,6 @@ func (v *Issuer) connect(ctx context.Context) error {
return err
}

v.cli.SetToken(v.Token)
return nil
}

Expand All @@ -100,6 +106,12 @@ func (v *Issuer) Issue(ctx context.Context, commonName string, conf *certify.Cer
}
}

// Convert Token to AuthMethod, if no AuthMethod set,
// for backwards compatibility.
if v.AuthMethod == nil {
v.AuthMethod = ConstantToken(v.Token)
}

csrPEM, keyPEM, err := csr.FromCertConfig(commonName, conf)
if err != nil {
return nil, err
Expand Down Expand Up @@ -147,6 +159,13 @@ func (v Issuer) signCSR(ctx context.Context, opts csrOpts) (*api.Secret, error)
pkiMountName = v.Mount
}

// Update token immediately before making the request
token, err := v.AuthMethod.GetToken(ctx, v.cli)
if err != nil {
return nil, err
}
v.cli.SetToken(token)

r := v.cli.NewRequest("PUT", "/v1/"+pkiMountName+"/sign/"+v.Role)
if err := r.SetJSONBody(opts); err != nil {
return nil, err
Expand Down
74 changes: 67 additions & 7 deletions issuers/vault/vault_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import (
"github.com/johanbrandhorst/certify/issuers/vault/proto"
)

//go:generate protoc --go_out=plugins=grpc:./ ./proto/test.proto

type otherName struct {
TypeID asn1.ObjectIdentifier
Value string `asn1:"explicit,utf8"`
Expand Down Expand Up @@ -57,16 +59,14 @@ func getOtherNames(cert *x509.Certificate) (otherNames []otherName, err error) {
return otherNames, nil
}

//go:generate protoc --go_out=plugins=grpc:./ ./proto/test.proto

var _ = Describe("Vault Issuer", func() {
var iss certify.Issuer
var conf *certify.CertConfig

BeforeEach(func() {
iss = &vault.Issuer{
URL: vaultTLSConf.URL,
Token: vaultTLSConf.Token,
AuthMethod: vault.ConstantToken(vaultTLSConf.Token),
Role: vaultTLSConf.Role,
TLSConfig: &tls.Config{
RootCAs: vaultTLSConf.CertPool,
Expand Down Expand Up @@ -106,11 +106,36 @@ var _ = Describe("Vault Issuer", func() {
}))
})

Context("with no explicit AuthMethod set", func() {
It("still works", func() {
cn := "somename.com"

tlsCert, err := iss.Issue(context.Background(), cn, conf)
Expect(err).NotTo(HaveOccurred())

Expect(tlsCert.Leaf).NotTo(BeNil(), "tlsCert.Leaf should be populated by Issue to track expiry")
Expect(tlsCert.Leaf.Subject.CommonName).To(Equal(cn))

// Check that chain is included
Expect(tlsCert.Certificate).To(HaveLen(2))
caCert, err := x509.ParseCertificate(tlsCert.Certificate[1])
Expect(err).NotTo(HaveOccurred())
Expect(caCert.Subject.SerialNumber).To(Equal(tlsCert.Leaf.Issuer.SerialNumber))

Expect(tlsCert.Leaf.NotBefore).To(BeTemporally("<", time.Now()))
Expect(tlsCert.Leaf.NotAfter).To(BeTemporally("~", time.Now().Add(iss.(*vault.Issuer).TimeToLive), 5*time.Second))
Expect(getOtherNames(tlsCert.Leaf)).To(ConsistOf(otherName{
TypeID: asn1.ObjectIdentifier([]int{1, 3, 6, 1, 4, 1, 311, 20, 2, 3}),
Value: "devops@nope.com",
}))
})
})

Context("with URI SANs", func() {
BeforeEach(func() {
iss = &vault.Issuer{
URL: vaultTLSConf.URL,
Token: vaultTLSConf.Token,
AuthMethod: vault.ConstantToken(vaultTLSConf.Token),
Role: vaultTLSConf.RoleURISANs,
TLSConfig: &tls.Config{
RootCAs: vaultTLSConf.CertPool,
Expand Down Expand Up @@ -148,7 +173,7 @@ var _ = Describe("Vault Issuer", func() {
BeforeEach(func() {
iss = &vault.Issuer{
URL: vaultTLSConf.URL,
Token: vaultTLSConf.Token,
AuthMethod: vault.ConstantToken(vaultTLSConf.Token),
Mount: altMount,
Role: vaultTLSConf.Role,
TLSConfig: &tls.Config{
Expand Down Expand Up @@ -252,7 +277,7 @@ var _ = Describe("Vault HTTP Issuer", func() {
BeforeEach(func() {
iss = &vault.Issuer{
URL: vaultConf.URL,
Token: vaultConf.Token,
AuthMethod: vault.ConstantToken(vaultTLSConf.Token),
Role: vaultConf.Role,
TimeToLive: time.Minute * 10,
// Format is "<type_id>;utf8:<value>", where type_id
Expand Down Expand Up @@ -388,6 +413,41 @@ var _ = Describe("Using a pre-created client", func() {
})
})

var _ = Describe("When an AuthMethod is not explicitly set", func() {
It("still works", func() {
iss := &vault.Issuer{
URL: vaultTLSConf.URL,
Token: vaultTLSConf.Token,
Role: vaultTLSConf.Role,
TLSConfig: &tls.Config{
RootCAs: vaultTLSConf.CertPool,
},
TimeToLive: time.Minute * 10,
}
conf := &certify.CertConfig{
KeyGenerator: keyGeneratorFunc(func() (crypto.PrivateKey, error) {
return ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
}),
}
cn := "somename.com"

tlsCert, err := iss.Issue(context.Background(), cn, conf)
Expect(err).NotTo(HaveOccurred())

Expect(tlsCert.Leaf).NotTo(BeNil(), "tlsCert.Leaf should be populated by Issue to track expiry")
Expect(tlsCert.Leaf.Subject.CommonName).To(Equal(cn))

// Check that chain is included
Expect(tlsCert.Certificate).To(HaveLen(2))
caCert, err := x509.ParseCertificate(tlsCert.Certificate[1])
Expect(err).NotTo(HaveOccurred())
Expect(caCert.Subject.SerialNumber).To(Equal(tlsCert.Leaf.Issuer.SerialNumber))

Expect(tlsCert.Leaf.NotBefore).To(BeTemporally("<", time.Now()))
Expect(tlsCert.Leaf.NotAfter).To(BeTemporally("~", time.Now().Add(iss.TimeToLive), 5*time.Second))
})
})

type keyGeneratorFunc func() (crypto.PrivateKey, error)

func (kgf keyGeneratorFunc) Generate() (crypto.PrivateKey, error) {
Expand Down Expand Up @@ -424,7 +484,7 @@ var _ = Describe("gRPC Test", func() {
CommonName: "Certify",
Issuer: &vault.Issuer{
URL: vaultTLSConf.URL,
Token: vaultTLSConf.Token,
AuthMethod: vault.ConstantToken(vaultTLSConf.Token),
Role: vaultTLSConf.Role,
TLSConfig: &tls.Config{
RootCAs: vaultTLSConf.CertPool,
Expand Down

0 comments on commit 6a8c57c

Please sign in to comment.