Merge branch 'master' into context-authority

pull/914/head
Mariano Cano 2 years ago
commit 26dd97e718

@ -0,0 +1,56 @@
name: Bug Report
description: File a bug report
title: "[Bug]: "
labels: ["bug", "needs triage"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
- type: textarea
id: steps
attributes:
label: Steps to Reproduce
description: Tell us how to reproduce this issue.
placeholder: These are the steps!
validations:
required: true
- type: textarea
id: your-env
attributes:
label: Your Environment
value: |-
* OS -
* `step-ca` Version -
validations:
required: true
- type: textarea
id: expected-behavior
attributes:
label: Expected Behavior
description: What did you expect to happen?
validations:
required: true
- type: textarea
id: actual-behavior
attributes:
label: Actual Behavior
description: What happens instead?
validations:
required: true
- type: textarea
id: context
attributes:
label: Additional Context
description: Add any other context about the problem here.
validations:
required: false
- type: textarea
id: contributing
attributes:
label: Contributing
value: |
Vote on this issue by adding a 👍 reaction.
To contribute a fix for this issue, leave a comment (and link to your pull request, if you've opened one already).
validations:
required: false

@ -1,27 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug, needs triage
assignees: ''
---
### Subject of the issue
Describe your issue here.
### Your environment
* OS -
* Version -
### Steps to reproduce
Tell us how to reproduce this issue. Please provide a working demo, you can use [this template](https://plnkr.co/edit/XorWgI?p=preview) as a base.
### Expected behaviour
Tell us what should happen
### Actual behaviour
Tell us what happens instead
### Additional context
Add any other context about the problem here.

@ -1,12 +1,20 @@
---
name: Documentation Request
about: Request documentation for a feature
title: ''
labels: documentation, needs triage
title: '[Docs]:'
labels: docs, needs triage
assignees: ''
---
## Hello!
<!-- Please leave this section as-is, it's designed to help others in the community know how to interact with our GitHub issues. -->
- Vote on this issue by adding a 👍 reaction
- If you want to document this feature, comment to let us know (we'll work with you on design, scheduling, etc.)
## Affected area/feature
<!---
Tell us which feature you'd like to see documented.
- Where would you like that documentation to live (command line usage output, website, github markdown on the repo)?

@ -1,13 +1,24 @@
---
name: Enhancement
about: Suggest an enhancement to step certificates
about: Suggest an enhancement to step-ca
title: ''
labels: enhancement, needs triage
assignees: ''
---
### What would you like to be added
## Hello!
<!-- Please leave this section as-is,
it's designed to help others in the community know how to interact with our GitHub issues. -->
- Vote on this issue by adding a 👍 reaction
- If you want to implement this feature, comment to let us know (we'll work with you on design, scheduling, etc.)
### Why this is needed
## Issue details
<!-- Enhancement requests are most helpful when they describe the problem you're having
as well as articulating the potential solution you'd like to see built. -->
## Why is this needed?
<!-- Let us know why you think this enhancement would be good for the project or community. -->

@ -33,7 +33,7 @@ jobs:
uses: golangci/golangci-lint-action@v2
with:
# Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
version: 'v1.45.0'
version: 'v1.45.2'
# Optional: working directory, useful for monorepos
# working-directory: somedir

@ -33,7 +33,7 @@ jobs:
uses: golangci/golangci-lint-action@v2
with:
# Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
version: 'v1.45.0'
version: 'v1.45.2'
# Optional: working directory, useful for monorepos
# working-directory: somedir

@ -151,7 +151,7 @@ integration: bin/$(BINNAME)
#########################################
fmt:
$Q gofmt -l -w $(SRC)
$Q gofmt -l -s -w $(SRC)
lint:
$Q golangci-lint run --timeout=30m

@ -49,7 +49,7 @@ func (a *Authority) StoreAdmin(ctx context.Context, adm *linkedca.Admin, prov pr
return admin.WrapErrorISE(err, "error creating admin")
}
if err := a.admins.Store(adm, prov); err != nil {
if err := a.reloadAdminResources(ctx); err != nil {
if err := a.ReloadAdminResources(ctx); err != nil {
return admin.WrapErrorISE(err, "error reloading admin resources on failed admin store")
}
return admin.WrapErrorISE(err, "error storing admin in authority cache")
@ -66,7 +66,7 @@ func (a *Authority) UpdateAdmin(ctx context.Context, id string, nu *linkedca.Adm
return nil, admin.WrapErrorISE(err, "error updating cached admin %s", id)
}
if err := a.adminDB.UpdateAdmin(ctx, adm); err != nil {
if err := a.reloadAdminResources(ctx); err != nil {
if err := a.ReloadAdminResources(ctx); err != nil {
return nil, admin.WrapErrorISE(err, "error reloading admin resources on failed admin update")
}
return nil, admin.WrapErrorISE(err, "error updating admin %s", id)
@ -88,7 +88,7 @@ func (a *Authority) removeAdmin(ctx context.Context, id string) error {
return admin.WrapErrorISE(err, "error removing admin %s from authority cache", id)
}
if err := a.adminDB.DeleteAdmin(ctx, id); err != nil {
if err := a.reloadAdminResources(ctx); err != nil {
if err := a.ReloadAdminResources(ctx); err != nil {
return admin.WrapErrorISE(err, "error reloading admin resources on failed admin remove")
}
return admin.WrapErrorISE(err, "error deleting admin %s", id)

@ -84,8 +84,12 @@ type Authority struct {
policyEngine *policy.Engine
adminMutex sync.RWMutex
// Do Not initialize the authority
skipInit bool
}
// Info contains information about the authority.
type Info struct {
StartTime time.Time
RootX509Certs []*x509.Certificate
@ -113,9 +117,11 @@ func New(cfg *config.Config, opts ...Option) (*Authority, error) {
}
}
// Initialize authority from options or configuration.
if err := a.init(); err != nil {
return nil, err
if !a.skipInit {
// Initialize authority from options or configuration.
if err := a.init(); err != nil {
return nil, err
}
}
return a, nil
@ -151,9 +157,11 @@ func NewEmbedded(opts ...Option) (*Authority, error) {
// Initialize config required fields.
a.config.Init()
// Initialize authority from options or configuration.
if err := a.init(); err != nil {
return nil, err
if !a.skipInit {
// Initialize authority from options or configuration.
if err := a.init(); err != nil {
return nil, err
}
}
return a, nil
@ -182,8 +190,8 @@ func MustFromContext(ctx context.Context) *Authority {
}
}
// reloadAdminResources reloads admins and provisioners from the DB.
func (a *Authority) reloadAdminResources(ctx context.Context) error {
// ReloadAdminResources reloads admins and provisioners from the DB.
func (a *Authority) ReloadAdminResources(ctx context.Context) error {
var (
provList provisioner.List
adminList []*linkedca.Admin
@ -582,7 +590,7 @@ func (a *Authority) init() error {
}
// Load Provisioners and Admins
if err := a.reloadAdminResources(ctx); err != nil {
if err := a.ReloadAdminResources(context.Background()); err != nil {
return err
}
@ -632,6 +640,12 @@ func (a *Authority) GetAdminDatabase() admin.DB {
return a.adminDB
}
// GetConfig returns the config.
func (a *Authority) GetConfig() *config.Config {
return a.config
}
// GetInfo returns information about the authority.
func (a *Authority) GetInfo() Info {
ai := Info{
StartTime: a.startTime,

@ -1034,7 +1034,7 @@ func TestAuthority_authorizeSSHSign(t *testing.T) {
}
} else {
if assert.Nil(t, tc.err) {
assert.Len(t, 8, got) // number of provisioner.SignOptions returned
assert.Len(t, 9, got) // number of provisioner.SignOptions returned
}
}
})

@ -289,18 +289,29 @@ func (c *linkedCaClient) StoreRenewedCertificate(parent *x509.Certificate, fullc
PemCertificateChain: serializeCertificateChain(fullchain[1:]...),
PemParentCertificate: serializeCertificateChain(parent),
})
return errors.Wrap(err, "error posting certificate")
return errors.Wrap(err, "error posting renewed certificate")
}
func (c *linkedCaClient) StoreSSHCertificate(crt *ssh.Certificate) error {
func (c *linkedCaClient) StoreSSHCertificate(prov provisioner.Interface, crt *ssh.Certificate) error {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
_, err := c.client.PostSSHCertificate(ctx, &linkedca.SSHCertificateRequest{
Certificate: string(ssh.MarshalAuthorizedKey(crt)),
Provisioner: createProvisionerIdentity(prov),
})
return errors.Wrap(err, "error posting ssh certificate")
}
func (c *linkedCaClient) StoreRenewedSSHCertificate(parent, crt *ssh.Certificate) error {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
_, err := c.client.PostSSHCertificate(ctx, &linkedca.SSHCertificateRequest{
Certificate: string(ssh.MarshalAuthorizedKey(crt)),
ParentCertificate: string(ssh.MarshalAuthorizedKey(parent)),
})
return errors.Wrap(err, "error posting renewed ssh certificate")
}
func (c *linkedCaClient) Revoke(crt *x509.Certificate, rci *db.RevokedCertificateInfo) error {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()

@ -266,6 +266,16 @@ func WithAdminDB(d admin.DB) Option {
}
}
// WithProvisioners is an option to set the provisioner collection.
//
// Deprecated: provisioner collections will likely change
func WithProvisioners(ps *provisioner.Collection) Option {
return func(a *Authority) error {
a.provisioners = ps
return nil
}
}
// WithLinkedCAToken is an option to set the authentication token used to enable
// linked ca.
func WithLinkedCAToken(token string) Option {
@ -284,6 +294,15 @@ func WithX509Enforcers(ces ...provisioner.CertificateEnforcer) Option {
}
}
// WithSkipInit is an option that allows the constructor to skip initializtion
// of the authority.
func WithSkipInit() Option {
return func(a *Authority) error {
a.skipInit = true
return nil
}
}
func readCertificateBundle(pemCerts []byte) ([]*x509.Certificate, error) {
var block *pem.Block
var certs []*x509.Certificate

@ -747,6 +747,7 @@ func (p *AWS) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption,
signOptions = append(signOptions, templateOptions)
return append(signOptions,
p,
// Validate user SignSSHOptions.
sshCertOptionsValidator(defaults),
// Set the validity bounds if not set.

@ -813,7 +813,6 @@ func TestAWS_AuthorizeSSHSign(t *testing.T) {
} else if assert.NotNil(t, got) {
cert, err := signSSHCertificate(tt.args.key, tt.args.sshOpts, got, signer.Key.(crypto.Signer))
if (err != nil) != tt.wantSignErr {
t.Errorf("SignSSH error = %v, wantSignErr %v", err, tt.wantSignErr)
} else {
if tt.wantSignErr {

@ -418,6 +418,7 @@ func (p *Azure) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOptio
signOptions = append(signOptions, templateOptions)
return append(signOptions,
p,
// Validate user SignSSHOptions.
sshCertOptionsValidator(defaults),
// Set the validity bounds if not set.

@ -3,6 +3,7 @@ package provisioner
import (
"context"
"crypto/x509"
"net/http"
"regexp"
"strings"
"time"
@ -131,7 +132,9 @@ func DefaultAuthorizeRenew(ctx context.Context, p *Controller, cert *x509.Certif
return errs.Unauthorized("certificate is not yet valid" + " " + now.UTC().Format(time.RFC3339Nano) + " vs " + cert.NotBefore.Format(time.RFC3339Nano))
}
if now.After(cert.NotAfter) && !p.Claimer.AllowRenewalAfterExpiry() {
return errs.Unauthorized("certificate has expired")
// return a custom 401 Unauthorized error with a clearer message for the client
// TODO(hs): these errors likely need to be refactored as a whole; HTTP status codes shouldn't be in this layer.
return errs.New(http.StatusUnauthorized, "The request lacked necessary authorization to be completed: certificate expired on %s", cert.NotAfter)
}
return nil

@ -425,6 +425,7 @@ func (p *GCP) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption,
signOptions = append(signOptions, templateOptions)
return append(signOptions,
p,
// Validate user SignSSHOptions.
sshCertOptionsValidator(defaults),
// Set the validity bounds if not set.

@ -257,6 +257,7 @@ func (p *JWK) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption,
}
return append(signOptions,
p,
// Set the validity bounds if not set.
&sshDefaultDuration{p.ctl.Claimer},
// Validate public key

@ -275,6 +275,7 @@ func (p *K8sSA) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOptio
signOptions := []SignOption{templateOptions}
return append(signOptions,
p,
// Require type, key-id and principals in the SignSSHOptions.
&sshCertOptionsRequireValidator{CertType: true, KeyID: true, Principals: true},
// Set the validity bounds if not set.

@ -368,9 +368,10 @@ func TestK8sSA_AuthorizeSSHSign(t *testing.T) {
} else {
if assert.Nil(t, tc.err) {
if assert.NotNil(t, opts) {
assert.Len(t, 7, opts)
assert.Len(t, 8, opts)
for _, o := range opts {
switch v := o.(type) {
case Interface:
case sshCertificateOptionsFunc:
case *sshCertOptionsRequireValidator:
assert.Equals(t, v, &sshCertOptionsRequireValidator{CertType: true, KeyID: true, Principals: true})

@ -250,6 +250,7 @@ func (p *Nebula) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOpti
}
return append(signOptions,
p,
templateOptions,
// Checks the validity bounds, and set the validity if has not been set.
&sshLimitDuration{p.ctl.Claimer, crt.Details.NotAfter},

@ -50,7 +50,7 @@ func (p *noop) AuthorizeRevoke(ctx context.Context, token string) error {
}
func (p *noop) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, error) {
return []SignOption{}, nil
return []SignOption{p}, nil
}
func (p *noop) AuthorizeSSHRenew(ctx context.Context, token string) (*ssh.Certificate, error) {

@ -434,6 +434,7 @@ func (o *OIDC) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption
}
return append(signOptions,
o,
// Set the validity bounds if not set.
&sshDefaultDuration{o.ctl.Claimer},
// Validate public key

@ -53,6 +53,7 @@ func signSSHCertificate(key crypto.PublicKey, opts SignSSHOptions, signOpts []Si
for _, op := range signOpts {
switch o := op.(type) {
case Interface:
// add options to NewCertificate
case SSHCertificateOptions:
certOptions = append(certOptions, o.Options(opts)...)

@ -312,6 +312,7 @@ func (p *X5C) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption,
}
return append(signOptions,
p,
// Checks the validity bounds, and set the validity if has not been set.
&sshLimitDuration{p.ctl.Claimer, claims.chains[0][0].NotAfter},
// Validate public key.

@ -769,6 +769,7 @@ func TestX5C_AuthorizeSSHSign(t *testing.T) {
nw := now()
for _, o := range opts {
switch v := o.(type) {
case Interface:
case sshCertOptionsValidator:
tc.claims.Step.SSH.ValidAfter.t = time.Time{}
tc.claims.Step.SSH.ValidBefore.t = time.Time{}
@ -799,9 +800,9 @@ func TestX5C_AuthorizeSSHSign(t *testing.T) {
tot++
}
if len(tc.claims.Step.SSH.CertType) > 0 {
assert.Equals(t, tot, 10)
assert.Equals(t, tot, 11)
} else {
assert.Equals(t, tot, 8)
assert.Equals(t, tot, 9)
}
}
}

@ -148,7 +148,7 @@ func (a *Authority) generateProvisionerConfig(ctx context.Context) (provisioner.
}
// StoreProvisioner stores an provisioner.Interface to the authority.
// StoreProvisioner stores a provisioner to the authority.
func (a *Authority) StoreProvisioner(ctx context.Context, prov *linkedca.Provisioner) error {
a.adminMutex.Lock()
defer a.adminMutex.Unlock()
@ -198,7 +198,7 @@ func (a *Authority) StoreProvisioner(ctx context.Context, prov *linkedca.Provisi
}
if err := a.provisioners.Store(certProv); err != nil {
if err := a.reloadAdminResources(ctx); err != nil {
if err := a.ReloadAdminResources(ctx); err != nil {
return admin.WrapErrorISE(err, "error reloading admin resources on failed provisioner store")
}
return admin.WrapErrorISE(err, "error storing provisioner in authority cache")
@ -234,7 +234,7 @@ func (a *Authority) UpdateProvisioner(ctx context.Context, nu *linkedca.Provisio
return admin.WrapErrorISE(err, "error updating provisioner '%s' in authority cache", nu.Name)
}
if err := a.adminDB.UpdateProvisioner(ctx, nu); err != nil {
if err := a.reloadAdminResources(ctx); err != nil {
if err := a.ReloadAdminResources(ctx); err != nil {
return admin.WrapErrorISE(err, "error reloading admin resources on failed provisioner update")
}
return admin.WrapErrorISE(err, "error updating provisioner '%s'", nu.Name)
@ -254,31 +254,33 @@ func (a *Authority) RemoveProvisioner(ctx context.Context, id string) error {
}
provName, provID := p.GetName(), p.GetID()
// Validate
// - Check that there will be SUPER_ADMINs that remain after we
// remove this provisioner.
if a.admins.SuperCount() == a.admins.SuperCountByProvisioner(provName) {
return admin.NewError(admin.ErrorBadRequestType,
"cannot remove provisioner %s because no super admins will remain", provName)
}
if a.IsAdminAPIEnabled() {
// Validate
// - Check that there will be SUPER_ADMINs that remain after we
// remove this provisioner.
if a.IsAdminAPIEnabled() && a.admins.SuperCount() == a.admins.SuperCountByProvisioner(provName) {
return admin.NewError(admin.ErrorBadRequestType,
"cannot remove provisioner %s because no super admins will remain", provName)
}
// Delete all admins associated with the provisioner.
admins, ok := a.admins.LoadByProvisioner(provName)
if ok {
for _, adm := range admins {
if err := a.removeAdmin(ctx, adm.Id); err != nil {
return admin.WrapErrorISE(err, "error deleting admin %s, as part of provisioner %s deletion", adm.Subject, provName)
// Delete all admins associated with the provisioner.
admins, ok := a.admins.LoadByProvisioner(provName)
if ok {
for _, adm := range admins {
if err := a.removeAdmin(ctx, adm.Id); err != nil {
return admin.WrapErrorISE(err, "error deleting admin %s, as part of provisioner %s deletion", adm.Subject, provName)
}
}
}
}
// Remove provisioner from authority caches.
if err := a.provisioners.Remove(provID); err != nil {
return admin.WrapErrorISE(err, "error removing admin from authority cache")
return admin.WrapErrorISE(err, "error removing provisioner from authority cache")
}
// Remove provisioner from database.
if err := a.adminDB.DeleteProvisioner(ctx, provID); err != nil {
if err := a.reloadAdminResources(ctx); err != nil {
if err := a.ReloadAdminResources(ctx); err != nil {
return admin.WrapErrorISE(err, "error reloading admin resources on failed provisioner remove")
}
return admin.WrapErrorISE(err, "error deleting provisioner %s", provName)

@ -161,8 +161,13 @@ func (a *Authority) SignSSH(ctx context.Context, key ssh.PublicKey, opts provisi
// Set backdate with the configured value
opts.Backdate = a.config.AuthorityConfig.Backdate.Duration
var prov provisioner.Interface
for _, op := range signOpts {
switch o := op.(type) {
// Capture current provisioner
case provisioner.Interface:
prov = o
// add options to NewCertificate
case provisioner.SSHCertificateOptions:
certOptions = append(certOptions, o.Options(opts)...)
@ -276,7 +281,7 @@ func (a *Authority) SignSSH(ctx context.Context, key ssh.PublicKey, opts provisi
}
}
if err = a.storeSSHCertificate(cert); err != nil && err != db.ErrNotImplemented {
if err = a.storeSSHCertificate(prov, cert); err != nil && err != db.ErrNotImplemented {
return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.SignSSH: error storing certificate in db")
}
@ -340,7 +345,7 @@ func (a *Authority) RenewSSH(ctx context.Context, oldCert *ssh.Certificate) (*ss
return nil, errs.Wrap(http.StatusInternalServerError, err, "signSSH: error signing certificate")
}
if err = a.storeSSHCertificate(cert); err != nil && err != db.ErrNotImplemented {
if err = a.storeRenewedSSHCertificate(oldCert, cert); err != nil && err != db.ErrNotImplemented {
return nil, errs.Wrap(http.StatusInternalServerError, err, "renewSSH: error storing certificate in db")
}
@ -419,21 +424,59 @@ func (a *Authority) RekeySSH(ctx context.Context, oldCert *ssh.Certificate, pub
}
}
if err = a.storeSSHCertificate(cert); err != nil && err != db.ErrNotImplemented {
if err = a.storeRenewedSSHCertificate(oldCert, cert); err != nil && err != db.ErrNotImplemented {
return nil, errs.Wrap(http.StatusInternalServerError, err, "rekeySSH; error storing certificate in db")
}
return cert, nil
}
func (a *Authority) storeSSHCertificate(cert *ssh.Certificate) error {
func (a *Authority) storeSSHCertificate(prov provisioner.Interface, cert *ssh.Certificate) error {
type sshCertificateStorer interface {
StoreSSHCertificate(crt *ssh.Certificate) error
StoreSSHCertificate(provisioner.Interface, *ssh.Certificate) error
}
// Store certificate in admindb or linkedca
switch s := a.adminDB.(type) {
case sshCertificateStorer:
return s.StoreSSHCertificate(prov, cert)
case db.CertificateStorer:
return s.StoreSSHCertificate(cert)
}
// Store certificate in localdb
switch s := a.db.(type) {
case sshCertificateStorer:
return s.StoreSSHCertificate(prov, cert)
case db.CertificateStorer:
return s.StoreSSHCertificate(cert)
default:
return nil
}
if s, ok := a.adminDB.(sshCertificateStorer); ok {
}
func (a *Authority) storeRenewedSSHCertificate(parent, cert *ssh.Certificate) error {
type sshRenewerCertificateStorer interface {
StoreRenewedSSHCertificate(parent, cert *ssh.Certificate) error
}
// Store certificate in admindb or linkedca
switch s := a.adminDB.(type) {
case sshRenewerCertificateStorer:
return s.StoreRenewedSSHCertificate(parent, cert)
case db.CertificateStorer:
return s.StoreSSHCertificate(cert)
}
return a.db.StoreSSHCertificate(cert)
// Store certificate in localdb
switch s := a.db.(type) {
case sshRenewerCertificateStorer:
return s.StoreRenewedSSHCertificate(parent, cert)
case db.CertificateStorer:
return s.StoreSSHCertificate(cert)
default:
return nil
}
}
// IsValidForAddUser checks if a user provisioner certificate can be issued to
@ -511,7 +554,7 @@ func (a *Authority) SignSSHAddUser(ctx context.Context, key ssh.PublicKey, subje
}
cert.Signature = sig
if err = a.storeSSHCertificate(cert); err != nil && err != db.ErrNotImplemented {
if err = a.storeRenewedSSHCertificate(subject, cert); err != nil && err != db.ErrNotImplemented {
return nil, errs.Wrap(http.StatusInternalServerError, err, "signSSHAddUser: error storing certificate in db")
}

@ -365,28 +365,31 @@ func (a *Authority) Rekey(oldCert *x509.Certificate, pk crypto.PublicKey) ([]*x5
// `StoreCertificate(...*x509.Certificate) error` instead of just
// `StoreCertificate(*x509.Certificate) error`.
func (a *Authority) storeCertificate(prov provisioner.Interface, fullchain []*x509.Certificate) error {
type linkedChainStorer interface {
type certificateChainStorer interface {
StoreCertificateChain(provisioner.Interface, ...*x509.Certificate) error
}
type certificateChainStorer interface {
type certificateChainSimpleStorer interface {
StoreCertificateChain(...*x509.Certificate) error
}
// Store certificate in linkedca
switch s := a.adminDB.(type) {
case linkedChainStorer:
return s.StoreCertificateChain(prov, fullchain...)
case certificateChainStorer:
return s.StoreCertificateChain(prov, fullchain...)
case certificateChainSimpleStorer:
return s.StoreCertificateChain(fullchain...)
}
// Store certificate in local db
switch s := a.db.(type) {
case linkedChainStorer:
return s.StoreCertificateChain(prov, fullchain...)
case certificateChainStorer:
return s.StoreCertificateChain(prov, fullchain...)
case certificateChainSimpleStorer:
return s.StoreCertificateChain(fullchain...)
case db.CertificateStorer:
return s.StoreCertificate(fullchain[0])
default:
return a.db.StoreCertificate(fullchain[0])
return nil
}
}
@ -398,15 +401,21 @@ func (a *Authority) storeRenewedCertificate(oldCert *x509.Certificate, fullchain
type renewedCertificateChainStorer interface {
StoreRenewedCertificate(*x509.Certificate, ...*x509.Certificate) error
}
// Store certificate in linkedca
if s, ok := a.adminDB.(renewedCertificateChainStorer); ok {
return s.StoreRenewedCertificate(oldCert, fullchain...)
}
// Store certificate in local db
if s, ok := a.db.(renewedCertificateChainStorer); ok {
switch s := a.db.(type) {
case renewedCertificateChainStorer:
return s.StoreRenewedCertificate(oldCert, fullchain...)
case db.CertificateStorer:
return s.StoreCertificate(fullchain[0])
default:
return nil
}
return a.db.StoreCertificate(fullchain[0])
}
// RevokeOptions are the options for the Revoke API.

@ -366,19 +366,19 @@ retry:
// GetProvisioner performs the GET /admin/provisioners/{name} request to the CA.
func (c *AdminClient) GetProvisioner(opts ...ProvisionerOption) (*linkedca.Provisioner, error) {
var retried bool
o := new(provisionerOptions)
if err := o.apply(opts); err != nil {
o := new(ProvisionerOptions)
if err := o.Apply(opts); err != nil {
return nil, err
}
var u *url.URL
switch {
case len(o.id) > 0:
case o.ID != "":
u = c.endpoint.ResolveReference(&url.URL{
Path: "/admin/provisioners/id",
RawQuery: o.rawQuery(),
})
case len(o.name) > 0:
u = c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "provisioners", o.name)})
case o.Name != "":
u = c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "provisioners", o.Name)})
default:
return nil, errors.New("must set either name or id in method options")
}
@ -413,8 +413,8 @@ retry:
// GetProvisionersPaginate performs the GET /admin/provisioners request to the CA.
func (c *AdminClient) GetProvisionersPaginate(opts ...ProvisionerOption) (*adminAPI.GetProvisionersResponse, error) {
var retried bool
o := new(provisionerOptions)
if err := o.apply(opts); err != nil {
o := new(ProvisionerOptions)
if err := o.Apply(opts); err != nil {
return nil, err
}
u := c.endpoint.ResolveReference(&url.URL{
@ -475,19 +475,19 @@ func (c *AdminClient) RemoveProvisioner(opts ...ProvisionerOption) error {
retried bool
)
o := new(provisionerOptions)
if err := o.apply(opts); err != nil {
o := new(ProvisionerOptions)
if err := o.Apply(opts); err != nil {
return err
}
switch {
case len(o.id) > 0:
case o.ID != "":
u = c.endpoint.ResolveReference(&url.URL{
Path: path.Join(adminURLPrefix, "provisioners/id"),
RawQuery: o.rawQuery(),
})
case len(o.name) > 0:
u = c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "provisioners", o.name)})
case o.Name != "":
u = c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "provisioners", o.Name)})
default:
return errors.New("must set either name or id in method options")
}

@ -7,6 +7,7 @@ import (
"net"
"net/http"
"net/http/httptest"
"os"
"reflect"
"strings"
"sync"
@ -374,6 +375,9 @@ func TestBootstrapClient(t *testing.T) {
}
func TestBootstrapClientServerRotation(t *testing.T) {
if os.Getenv("CI") == "true" {
t.Skipf("skip until we fix https://github.com/smallstep/certificates/issues/873")
}
reset := setMinCertDuration(1 * time.Second)
defer reset()

@ -425,16 +425,18 @@ func parseEndpoint(endpoint string) (*url.URL, error) {
}
// ProvisionerOption is the type of options passed to the Provisioner method.
type ProvisionerOption func(o *provisionerOptions) error
type ProvisionerOption func(o *ProvisionerOptions) error
type provisionerOptions struct {
cursor string
limit int
id string
name string
// ProvisionerOptions stores options for the provisioner CRUD API.
type ProvisionerOptions struct {
Cursor string
Limit int
ID string
Name string
}
func (o *provisionerOptions) apply(opts []ProvisionerOption) (err error) {
// Apply caches provisioner options on a struct for later use.
func (o *ProvisionerOptions) Apply(opts []ProvisionerOption) (err error) {
for _, fn := range opts {
if err = fn(o); err != nil {
return
@ -443,51 +445,51 @@ func (o *provisionerOptions) apply(opts []ProvisionerOption) (err error) {
return
}
func (o *provisionerOptions) rawQuery() string {
func (o *ProvisionerOptions) rawQuery() string {
v := url.Values{}
if len(o.cursor) > 0 {
v.Set("cursor", o.cursor)
if o.Cursor != "" {
v.Set("cursor", o.Cursor)
}
if o.limit > 0 {
v.Set("limit", strconv.Itoa(o.limit))
if o.Limit > 0 {
v.Set("limit", strconv.Itoa(o.Limit))
}
if len(o.id) > 0 {
v.Set("id", o.id)
if o.ID != "" {
v.Set("id", o.ID)
}
if len(o.name) > 0 {
v.Set("name", o.name)
if o.Name != "" {
v.Set("name", o.Name)
}
return v.Encode()
}
// WithProvisionerCursor will request the provisioners starting with the given cursor.
func WithProvisionerCursor(cursor string) ProvisionerOption {
return func(o *provisionerOptions) error {
o.cursor = cursor
return func(o *ProvisionerOptions) error {
o.Cursor = cursor
return nil
}
}
// WithProvisionerLimit will request the given number of provisioners.
func WithProvisionerLimit(limit int) ProvisionerOption {
return func(o *provisionerOptions) error {
o.limit = limit
return func(o *ProvisionerOptions) error {
o.Limit = limit
return nil
}
}
// WithProvisionerID will request the given provisioner.
func WithProvisionerID(id string) ProvisionerOption {
return func(o *provisionerOptions) error {
o.id = id
return func(o *ProvisionerOptions) error {
o.ID = id
return nil
}
}
// WithProvisionerName will request the given provisioner.
func WithProvisionerName(name string) ProvisionerOption {
return func(o *provisionerOptions) error {
o.name = name
return func(o *ProvisionerOptions) error {
o.Name = name
return nil
}
}
@ -810,8 +812,8 @@ retry:
// paginate the provisioners.
func (c *Client) Provisioners(opts ...ProvisionerOption) (*api.ProvisionersResponse, error) {
var retried bool
o := new(provisionerOptions)
if err := o.apply(opts); err != nil {
o := new(ProvisionerOptions)
if err := o.Apply(opts); err != nil {
return nil, err
}
u := c.endpoint.ResolveReference(&url.URL{

@ -0,0 +1,67 @@
package approle
import (
"encoding/json"
"errors"
"fmt"
"github.com/hashicorp/vault/api/auth/approle"
)
// AuthOptions defines the configuration options added using the
// VaultOptions.AuthOptions field when AuthType is approle
type AuthOptions struct {
RoleID string `json:"roleID,omitempty"`
SecretID string `json:"secretID,omitempty"`
SecretIDFile string `json:"secretIDFile,omitempty"`
SecretIDEnv string `json:"secretIDEnv,omitempty"`
IsWrappingToken bool `json:"isWrappingToken,omitempty"`
}
func NewApproleAuthMethod(mountPath string, options json.RawMessage) (*approle.AppRoleAuth, error) {
var opts *AuthOptions
err := json.Unmarshal(options, &opts)
if err != nil {
return nil, fmt.Errorf("error decoding AppRole auth options: %w", err)
}
var approleAuth *approle.AppRoleAuth
var loginOptions []approle.LoginOption
if mountPath != "" {
loginOptions = append(loginOptions, approle.WithMountPath(mountPath))
}
if opts.IsWrappingToken {
loginOptions = append(loginOptions, approle.WithWrappingToken())
}
if opts.RoleID == "" {
return nil, errors.New("you must set roleID")
}
var sid approle.SecretID
switch {
case opts.SecretID != "" && opts.SecretIDFile == "" && opts.SecretIDEnv == "":
sid = approle.SecretID{
FromString: opts.SecretID,
}
case opts.SecretIDFile != "" && opts.SecretID == "" && opts.SecretIDEnv == "":
sid = approle.SecretID{
FromFile: opts.SecretIDFile,
}
case opts.SecretIDEnv != "" && opts.SecretIDFile == "" && opts.SecretID == "":
sid = approle.SecretID{
FromEnv: opts.SecretIDEnv,
}
default:
return nil, errors.New("you must set one of secretID, secretIDFile or secretIDEnv")
}
approleAuth, err = approle.NewAppRoleAuth(opts.RoleID, &sid, loginOptions...)
if err != nil {
return nil, fmt.Errorf("unable to initialize Kubernetes auth method: %w", err)
}
return approleAuth, nil
}

@ -0,0 +1,195 @@
package approle
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"testing"
vault "github.com/hashicorp/vault/api"
)
func testCAHelper(t *testing.T) (*url.URL, *vault.Client) {
t.Helper()
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.RequestURI == "/v1/auth/approle/login":
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `{
"auth": {
"client_token": "hvs.0000"
}
}`)
case r.RequestURI == "/v1/auth/custom-approle/login":
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `{
"auth": {
"client_token": "hvs.9999"
}
}`)
default:
w.WriteHeader(http.StatusNotFound)
fmt.Fprintf(w, `{"error":"not found"}`)
}
}))
t.Cleanup(func() {
srv.Close()
})
u, err := url.Parse(srv.URL)
if err != nil {
srv.Close()
t.Fatal(err)
}
config := vault.DefaultConfig()
config.Address = srv.URL
client, err := vault.NewClient(config)
if err != nil {
srv.Close()
t.Fatal(err)
}
return u, client
}
func TestApprole_LoginMountPaths(t *testing.T) {
caURL, _ := testCAHelper(t)
config := vault.DefaultConfig()
config.Address = caURL.String()
client, _ := vault.NewClient(config)
tests := []struct {
name string
mountPath string
token string
}{
{
name: "ok default mount path",
mountPath: "",
token: "hvs.0000",
},
{
name: "ok explicit mount path",
mountPath: "approle",
token: "hvs.0000",
},
{
name: "ok custom mount path",
mountPath: "custom-approle",
token: "hvs.9999",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
method, err := NewApproleAuthMethod(tt.mountPath, json.RawMessage(`{"RoleID":"roleID","SecretID":"secretID","IsWrappingToken":false}`))
if err != nil {
t.Errorf("NewApproleAuthMethod() error = %v", err)
return
}
secret, err := client.Auth().Login(context.Background(), method)
if err != nil {
t.Errorf("Login() error = %v", err)
return
}
token, _ := secret.TokenID()
if token != tt.token {
t.Errorf("Token error got %v, expected %v", token, tt.token)
return
}
})
}
}
func TestApprole_NewApproleAuthMethod(t *testing.T) {
tests := []struct {
name string
mountPath string
raw string
wantErr bool
}{
{
"ok secret-id string",
"",
`{"RoleID": "0000-0000-0000-0000", "SecretID": "0000-0000-0000-0000"}`,
false,
},
{
"ok secret-id string and wrapped",
"",
`{"RoleID": "0000-0000-0000-0000", "SecretID": "0000-0000-0000-0000", "isWrappedToken": true}`,
false,
},
{
"ok secret-id string and wrapped with custom mountPath",
"approle2",
`{"RoleID": "0000-0000-0000-0000", "SecretID": "0000-0000-0000-0000", "isWrappedToken": true}`,
false,
},
{
"ok secret-id file",
"",
`{"RoleID": "0000-0000-0000-0000", "SecretIDFile": "./secret-id"}`,
false,
},
{
"ok secret-id env",
"",
`{"RoleID": "0000-0000-0000-0000", "SecretIDEnv": "VAULT_APPROLE_SECRETID"}`,
false,
},
{
"fail mandatory role-id",
"",
`{}`,
true,
},
{
"fail mandatory secret-id any",
"",
`{"RoleID": "0000-0000-0000-0000"}`,
true,
},
{
"fail multiple secret-id types id and env",
"",
`{"RoleID": "0000-0000-0000-0000", "SecretID": "0000-0000-0000-0000", "SecretIDEnv": "VAULT_APPROLE_SECRETID"}`,
true,
},
{
"fail multiple secret-id types id and file",
"",
`{"RoleID": "0000-0000-0000-0000", "SecretID": "0000-0000-0000-0000", "SecretIDFile": "./secret-id"}`,
true,
},
{
"fail multiple secret-id types env and file",
"",
`{"RoleID": "0000-0000-0000-0000", "SecretIDFile": "./secret-id", "SecretIDEnv": "VAULT_APPROLE_SECRETID"}`,
true,
},
{
"fail multiple secret-id types all",
"",
`{"RoleID": "0000-0000-0000-0000", "SecretID": "0000-0000-0000-0000", "SecretIDFile": "./secret-id", "SecretIDEnv": "VAULT_APPROLE_SECRETID"}`,
true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := NewApproleAuthMethod(tt.mountPath, json.RawMessage(tt.raw))
if (err != nil) != tt.wantErr {
t.Errorf("Approle.NewApproleAuthMethod() error = %v, wantErr %v", err, tt.wantErr)
return
}
})
}
}

@ -0,0 +1,49 @@
package kubernetes
import (
"encoding/json"
"errors"
"fmt"
"github.com/hashicorp/vault/api/auth/kubernetes"
)
// AuthOptions defines the configuration options added using the
// VaultOptions.AuthOptions field when AuthType is kubernetes
type AuthOptions struct {
Role string `json:"role,omitempty"`
TokenPath string `json:"tokenPath,omitempty"`
}
func NewKubernetesAuthMethod(mountPath string, options json.RawMessage) (*kubernetes.KubernetesAuth, error) {
var opts *AuthOptions
err := json.Unmarshal(options, &opts)
if err != nil {
return nil, fmt.Errorf("error decoding Kubernetes auth options: %w", err)
}
var kubernetesAuth *kubernetes.KubernetesAuth
var loginOptions []kubernetes.LoginOption
if mountPath != "" {
loginOptions = append(loginOptions, kubernetes.WithMountPath(mountPath))
}
if opts.TokenPath != "" {
loginOptions = append(loginOptions, kubernetes.WithServiceAccountTokenPath(opts.TokenPath))
}
if opts.Role == "" {
return nil, errors.New("you must set role")
}
kubernetesAuth, err = kubernetes.NewKubernetesAuth(
opts.Role,
loginOptions...,
)
if err != nil {
return nil, fmt.Errorf("unable to initialize Kubernetes auth method: %w", err)
}
return kubernetesAuth, nil
}

@ -0,0 +1,149 @@
package kubernetes
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"path"
"path/filepath"
"runtime"
"testing"
vault "github.com/hashicorp/vault/api"
)
func testCAHelper(t *testing.T) (*url.URL, *vault.Client) {
t.Helper()
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.RequestURI == "/v1/auth/kubernetes/login":
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `{
"auth": {
"client_token": "hvs.0000"
}
}`)
case r.RequestURI == "/v1/auth/custom-kubernetes/login":
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `{
"auth": {
"client_token": "hvs.9999"
}
}`)
default:
w.WriteHeader(http.StatusNotFound)
fmt.Fprintf(w, `{"error":"not found"}`)
}
}))
t.Cleanup(func() {
srv.Close()
})
u, err := url.Parse(srv.URL)
if err != nil {
srv.Close()
t.Fatal(err)
}
config := vault.DefaultConfig()
config.Address = srv.URL
client, err := vault.NewClient(config)
if err != nil {
srv.Close()
t.Fatal(err)
}
return u, client
}
func TestApprole_LoginMountPaths(t *testing.T) {
caURL, _ := testCAHelper(t)
_, filename, _, _ := runtime.Caller(0)
tokenPath := filepath.Join(path.Dir(filename), "token")
config := vault.DefaultConfig()
config.Address = caURL.String()
client, _ := vault.NewClient(config)
tests := []struct {
name string
mountPath string
token string
}{
{
name: "ok default mount path",
mountPath: "",
token: "hvs.0000",
},
{
name: "ok explicit mount path",
mountPath: "kubernetes",
token: "hvs.0000",
},
{
name: "ok custom mount path",
mountPath: "custom-kubernetes",
token: "hvs.9999",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
method, err := NewKubernetesAuthMethod(tt.mountPath, json.RawMessage(`{"role": "SomeRoleName", "tokenPath": "`+tokenPath+`"}`))
if err != nil {
t.Errorf("NewApproleAuthMethod() error = %v", err)
return
}
secret, err := client.Auth().Login(context.Background(), method)
if err != nil {
t.Errorf("Login() error = %v", err)
return
}
token, _ := secret.TokenID()
if token != tt.token {
t.Errorf("Token error got %v, expected %v", token, tt.token)
return
}
})
}
}
func TestApprole_NewApproleAuthMethod(t *testing.T) {
_, filename, _, _ := runtime.Caller(0)
tokenPath := filepath.Join(path.Dir(filename), "token")
tests := []struct {
name string
mountPath string
raw string
wantErr bool
}{
{
"ok secret-id string",
"",
`{"role": "SomeRoleName", "tokenPath": "` + tokenPath + `"}`,
false,
},
{
"fail mandatory role",
"",
`{}`,
true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := NewKubernetesAuthMethod(tt.mountPath, json.RawMessage(tt.raw))
if (err != nil) != tt.wantErr {
t.Errorf("Kubernetes.NewKubernetesAuthMethod() error = %v, wantErr %v", err, tt.wantErr)
return
}
})
}
}

@ -15,9 +15,10 @@ import (
"time"
"github.com/smallstep/certificates/cas/apiv1"
"github.com/smallstep/certificates/cas/vaultcas/auth/approle"
"github.com/smallstep/certificates/cas/vaultcas/auth/kubernetes"
vault "github.com/hashicorp/vault/api"
auth "github.com/hashicorp/vault/api/auth/approle"
)
func init() {
@ -29,15 +30,14 @@ func init() {
// VaultOptions defines the configuration options added using the
// apiv1.Options.Config field.
type VaultOptions struct {
PKI string `json:"pki,omitempty"`
PKIRoleDefault string `json:"pkiRoleDefault,omitempty"`
PKIRoleRSA string `json:"pkiRoleRSA,omitempty"`
PKIRoleEC string `json:"pkiRoleEC,omitempty"`
PKIRoleEd25519 string `json:"pkiRoleEd25519,omitempty"`
RoleID string `json:"roleID,omitempty"`
SecretID auth.SecretID `json:"secretID,omitempty"`
AppRole string `json:"appRole,omitempty"`
IsWrappingToken bool `json:"isWrappingToken,omitempty"`
PKIMountPath string `json:"pkiMountPath,omitempty"`
PKIRoleDefault string `json:"pkiRoleDefault,omitempty"`
PKIRoleRSA string `json:"pkiRoleRSA,omitempty"`
PKIRoleEC string `json:"pkiRoleEC,omitempty"`
PKIRoleEd25519 string `json:"pkiRoleEd25519,omitempty"`
AuthType string `json:"authType,omitempty"`
AuthMountPath string `json:"authMountPath,omitempty"`
AuthOptions json.RawMessage `json:"authOptions,omitempty"`
}
// VaultCAS implements a Certificate Authority Service using Hashicorp Vault.
@ -77,28 +77,22 @@ func New(ctx context.Context, opts apiv1.Options) (*VaultCAS, error) {
return nil, fmt.Errorf("unable to initialize vault client: %w", err)
}
var appRoleAuth *auth.AppRoleAuth
if vc.IsWrappingToken {
appRoleAuth, err = auth.NewAppRoleAuth(
vc.RoleID,
&vc.SecretID,
auth.WithWrappingToken(),
auth.WithMountPath(vc.AppRole),
)
} else {
appRoleAuth, err = auth.NewAppRoleAuth(
vc.RoleID,
&vc.SecretID,
auth.WithMountPath(vc.AppRole),
)
var method vault.AuthMethod
switch vc.AuthType {
case "kubernetes":
method, err = kubernetes.NewKubernetesAuthMethod(vc.AuthMountPath, vc.AuthOptions)
case "approle":
method, err = approle.NewApproleAuthMethod(vc.AuthMountPath, vc.AuthOptions)
default:
return nil, fmt.Errorf("unknown auth type: %s, only 'kubernetes' and 'approle' currently supported", vc.AuthType)
}
if err != nil {
return nil, fmt.Errorf("unable to initialize AppRole auth method: %w", err)
return nil, fmt.Errorf("unable to configure %s auth method: %w", vc.AuthType, err)
}
authInfo, err := client.Auth().Login(ctx, appRoleAuth)
authInfo, err := client.Auth().Login(ctx, method)
if err != nil {
return nil, fmt.Errorf("unable to login to AppRole auth method: %w", err)
return nil, fmt.Errorf("unable to login to %s auth method: %w", vc.AuthType, err)
}
if authInfo == nil {
return nil, errors.New("no auth info was returned after login")
@ -134,7 +128,7 @@ func (v *VaultCAS) CreateCertificate(req *apiv1.CreateCertificateRequest) (*apiv
// GetCertificateAuthority returns the root certificate of the certificate
// authority using the configured fingerprint.
func (v *VaultCAS) GetCertificateAuthority(req *apiv1.GetCertificateAuthorityRequest) (*apiv1.GetCertificateAuthorityResponse, error) {
secret, err := v.client.Logical().Read(v.config.PKI + "/cert/ca_chain")
secret, err := v.client.Logical().Read(v.config.PKIMountPath + "/cert/ca_chain")
if err != nil {
return nil, fmt.Errorf("error reading ca chain: %w", err)
}
@ -190,7 +184,7 @@ func (v *VaultCAS) RevokeCertificate(req *apiv1.RevokeCertificateRequest) (*apiv
vaultReq := map[string]interface{}{
"serial_number": formatSerialNumber(sn),
}
_, err := v.client.Logical().Write(v.config.PKI+"/revoke/", vaultReq)
_, err := v.client.Logical().Write(v.config.PKIMountPath+"/revoke/", vaultReq)
if err != nil {
return nil, fmt.Errorf("error revoking certificate: %w", err)
}
@ -224,7 +218,7 @@ func (v *VaultCAS) createCertificate(cr *x509.CertificateRequest, lifetime time.
"ttl": lifetime.Seconds(),
}
secret, err := v.client.Logical().Write(v.config.PKI+"/sign/"+vaultPKIRole, vaultReq)
secret, err := v.client.Logical().Write(v.config.PKIMountPath+"/sign/"+vaultPKIRole, vaultReq)
if err != nil {
return nil, nil, fmt.Errorf("error signing certificate: %w", err)
}
@ -247,21 +241,17 @@ func (v *VaultCAS) createCertificate(cr *x509.CertificateRequest, lifetime time.
}
func loadOptions(config json.RawMessage) (*VaultOptions, error) {
var vc *VaultOptions
// setup default values
vc := VaultOptions{
PKIMountPath: "pki",
PKIRoleDefault: "default",
}
err := json.Unmarshal(config, &vc)
if err != nil {
return nil, fmt.Errorf("error decoding vaultCAS config: %w", err)
}
if vc.PKI == "" {
vc.PKI = "pki" // use default pki vault name
}
if vc.PKIRoleDefault == "" {
vc.PKIRoleDefault = "default" // use default pki role name
}
if vc.PKIRoleRSA == "" {
vc.PKIRoleRSA = vc.PKIRoleDefault
}
@ -272,23 +262,7 @@ func loadOptions(config json.RawMessage) (*VaultOptions, error) {
vc.PKIRoleEd25519 = vc.PKIRoleDefault
}
if vc.RoleID == "" {
return nil, errors.New("vaultCAS config options must define `roleID`")
}
if vc.SecretID.FromEnv == "" && vc.SecretID.FromFile == "" && vc.SecretID.FromString == "" {
return nil, errors.New("vaultCAS config options must define `secretID` object with one of `FromEnv`, `FromFile` or `FromString`")
}
if vc.PKI == "" {
vc.PKI = "pki" // use default pki vault name
}
if vc.AppRole == "" {
vc.AppRole = "auth/approle"
}
return vc, nil
return &vc, nil
}
func parseCertificates(pemCert string) []*x509.Certificate {

@ -14,7 +14,6 @@ import (
"time"
vault "github.com/hashicorp/vault/api"
auth "github.com/hashicorp/vault/api/auth/approle"
"github.com/smallstep/certificates/cas/apiv1"
"go.step.sm/crypto/pemutil"
)
@ -99,7 +98,7 @@ func testCAHelper(t *testing.T) (*url.URL, *vault.Client) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.RequestURI == "/v1/auth/auth/approle/login":
case r.RequestURI == "/v1/auth/approle/login":
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `{
"auth": {
@ -183,11 +182,8 @@ func TestNew_register(t *testing.T) {
CertificateAuthority: caURL.String(),
CertificateAuthorityFingerprint: testRootFingerprint,
Config: json.RawMessage(`{
"PKI": "pki",
"PKIRoleDefault": "pki-role",
"RoleID": "roleID",
"SecretID": {"FromString": "secretID"},
"IsWrappingToken": false
"AuthType": "approle",
"AuthOptions": {"RoleID":"roleID","SecretID":"secretID","IsWrappingToken":false}
}`),
})
@ -201,15 +197,11 @@ func TestVaultCAS_CreateCertificate(t *testing.T) {
_, client := testCAHelper(t)
options := VaultOptions{
PKI: "pki",
PKIRoleDefault: "role",
PKIRoleRSA: "rsa",
PKIRoleEC: "ec",
PKIRoleEd25519: "ed25519",
RoleID: "roleID",
SecretID: auth.SecretID{FromString: "secretID"},
AppRole: "approle",
IsWrappingToken: false,
PKIMountPath: "pki",
PKIRoleDefault: "role",
PKIRoleRSA: "rsa",
PKIRoleEC: "ec",
PKIRoleEd25519: "ed25519",
}
type fields struct {
@ -291,7 +283,7 @@ func TestVaultCAS_GetCertificateAuthority(t *testing.T) {
}
options := VaultOptions{
PKI: "pki",
PKIMountPath: "pki",
}
rootCert := parseCertificates(testRootCertificate)[0]
@ -335,15 +327,11 @@ func TestVaultCAS_RevokeCertificate(t *testing.T) {
_, client := testCAHelper(t)
options := VaultOptions{
PKI: "pki",
PKIRoleDefault: "role",
PKIRoleRSA: "rsa",
PKIRoleEC: "ec",
PKIRoleEd25519: "ed25519",
RoleID: "roleID",
SecretID: auth.SecretID{FromString: "secretID"},
AppRole: "approle",
IsWrappingToken: false,
PKIMountPath: "pki",
PKIRoleDefault: "role",
PKIRoleRSA: "rsa",
PKIRoleEC: "ec",
PKIRoleEd25519: "ed25519",
}
type fields struct {
@ -407,15 +395,11 @@ func TestVaultCAS_RenewCertificate(t *testing.T) {
_, client := testCAHelper(t)
options := VaultOptions{
PKI: "pki",
PKIRoleDefault: "role",
PKIRoleRSA: "rsa",
PKIRoleEC: "ec",
PKIRoleEd25519: "ed25519",
RoleID: "roleID",
SecretID: auth.SecretID{FromString: "secretID"},
AppRole: "approle",
IsWrappingToken: false,
PKIMountPath: "pki",
PKIRoleDefault: "role",
PKIRoleRSA: "rsa",
PKIRoleEC: "ec",
PKIRoleEd25519: "ed25519",
}
type fields struct {
@ -464,202 +448,66 @@ func TestVaultCAS_loadOptions(t *testing.T) {
want *VaultOptions
wantErr bool
}{
{
"ok mandatory with SecretID FromString",
`{"RoleID": "roleID", "SecretID": {"FromString": "secretID"}}`,
&VaultOptions{
PKI: "pki",
PKIRoleDefault: "default",
PKIRoleRSA: "default",
PKIRoleEC: "default",
PKIRoleEd25519: "default",
RoleID: "roleID",
SecretID: auth.SecretID{FromString: "secretID"},
AppRole: "auth/approle",
IsWrappingToken: false,
},
false,
},
{
"ok mandatory with SecretID FromFile",
`{"RoleID": "roleID", "SecretID": {"FromFile": "secretID"}}`,
&VaultOptions{
PKI: "pki",
PKIRoleDefault: "default",
PKIRoleRSA: "default",
PKIRoleEC: "default",
PKIRoleEd25519: "default",
RoleID: "roleID",
SecretID: auth.SecretID{FromFile: "secretID"},
AppRole: "auth/approle",
IsWrappingToken: false,
},
false,
},
{
"ok mandatory with SecretID FromEnv",
`{"RoleID": "roleID", "SecretID": {"FromEnv": "secretID"}}`,
&VaultOptions{
PKI: "pki",
PKIRoleDefault: "default",
PKIRoleRSA: "default",
PKIRoleEC: "default",
PKIRoleEd25519: "default",
RoleID: "roleID",
SecretID: auth.SecretID{FromEnv: "secretID"},
AppRole: "auth/approle",
IsWrappingToken: false,
},
false,
},
{
"ok mandatory PKIRole PKIRoleEd25519",
`{"PKIRoleDefault": "role", "PKIRoleEd25519": "ed25519" , "RoleID": "roleID", "SecretID": {"FromEnv": "secretID"}}`,
`{"PKIRoleDefault": "role", "PKIRoleEd25519": "ed25519"}`,
&VaultOptions{
PKI: "pki",
PKIRoleDefault: "role",
PKIRoleRSA: "role",
PKIRoleEC: "role",
PKIRoleEd25519: "ed25519",
RoleID: "roleID",
SecretID: auth.SecretID{FromEnv: "secretID"},
AppRole: "auth/approle",
IsWrappingToken: false,
PKIMountPath: "pki",
PKIRoleDefault: "role",
PKIRoleRSA: "role",
PKIRoleEC: "role",
PKIRoleEd25519: "ed25519",
},
false,
},
{
"ok mandatory PKIRole PKIRoleEC",
`{"PKIRoleDefault": "role", "PKIRoleEC": "ec" , "RoleID": "roleID", "SecretID": {"FromEnv": "secretID"}}`,
`{"PKIRoleDefault": "role", "PKIRoleEC": "ec"}`,
&VaultOptions{
PKI: "pki",
PKIRoleDefault: "role",
PKIRoleRSA: "role",
PKIRoleEC: "ec",
PKIRoleEd25519: "role",
RoleID: "roleID",
SecretID: auth.SecretID{FromEnv: "secretID"},
AppRole: "auth/approle",
IsWrappingToken: false,
PKIMountPath: "pki",
PKIRoleDefault: "role",
PKIRoleRSA: "role",
PKIRoleEC: "ec",
PKIRoleEd25519: "role",
},
false,
},
{
"ok mandatory PKIRole PKIRoleRSA",
`{"PKIRoleDefault": "role", "PKIRoleRSA": "rsa" , "RoleID": "roleID", "SecretID": {"FromEnv": "secretID"}}`,
`{"PKIRoleDefault": "role", "PKIRoleRSA": "rsa"}`,
&VaultOptions{
PKI: "pki",
PKIRoleDefault: "role",
PKIRoleRSA: "rsa",
PKIRoleEC: "role",
PKIRoleEd25519: "role",
RoleID: "roleID",
SecretID: auth.SecretID{FromEnv: "secretID"},
AppRole: "auth/approle",
IsWrappingToken: false,
PKIMountPath: "pki",
PKIRoleDefault: "role",
PKIRoleRSA: "rsa",
PKIRoleEC: "role",
PKIRoleEd25519: "role",
},
false,
},
{
"ok mandatory PKIRoleRSA PKIRoleEC PKIRoleEd25519",
`{"PKIRoleRSA": "rsa", "PKIRoleEC": "ec", "PKIRoleEd25519": "ed25519", "RoleID": "roleID", "SecretID": {"FromEnv": "secretID"}}`,
`{"PKIRoleRSA": "rsa", "PKIRoleEC": "ec", "PKIRoleEd25519": "ed25519"}`,
&VaultOptions{
PKI: "pki",
PKIRoleDefault: "default",
PKIRoleRSA: "rsa",
PKIRoleEC: "ec",
PKIRoleEd25519: "ed25519",
RoleID: "roleID",
SecretID: auth.SecretID{FromEnv: "secretID"},
AppRole: "auth/approle",
IsWrappingToken: false,
PKIMountPath: "pki",
PKIRoleDefault: "default",
PKIRoleRSA: "rsa",
PKIRoleEC: "ec",
PKIRoleEd25519: "ed25519",
},
false,
},
{
"ok mandatory PKIRoleRSA PKIRoleEC PKIRoleEd25519 with useless PKIRoleDefault",
`{"PKIRoleDefault": "role", "PKIRoleRSA": "rsa", "PKIRoleEC": "ec", "PKIRoleEd25519": "ed25519", "RoleID": "roleID", "SecretID": {"FromEnv": "secretID"}}`,
`{"PKIRoleDefault": "role", "PKIRoleRSA": "rsa", "PKIRoleEC": "ec", "PKIRoleEd25519": "ed25519"}`,
&VaultOptions{
PKI: "pki",
PKIRoleDefault: "role",
PKIRoleRSA: "rsa",
PKIRoleEC: "ec",
PKIRoleEd25519: "ed25519",
RoleID: "roleID",
SecretID: auth.SecretID{FromEnv: "secretID"},
AppRole: "auth/approle",
IsWrappingToken: false,
PKIMountPath: "pki",
PKIRoleDefault: "role",
PKIRoleRSA: "rsa",
PKIRoleEC: "ec",
PKIRoleEd25519: "ed25519",
},
false,
},
{
"ok mandatory with AppRole",
`{"AppRole": "test", "RoleID": "roleID", "SecretID": {"FromString": "secretID"}}`,
&VaultOptions{
PKI: "pki",
PKIRoleDefault: "default",
PKIRoleRSA: "default",
PKIRoleEC: "default",
PKIRoleEd25519: "default",
RoleID: "roleID",
SecretID: auth.SecretID{FromString: "secretID"},
AppRole: "test",
IsWrappingToken: false,
},
false,
},
{
"ok mandatory with IsWrappingToken",
`{"IsWrappingToken": true, "RoleID": "roleID", "SecretID": {"FromString": "secretID"}}`,
&VaultOptions{
PKI: "pki",
PKIRoleDefault: "default",
PKIRoleRSA: "default",
PKIRoleEC: "default",
PKIRoleEd25519: "default",
RoleID: "roleID",
SecretID: auth.SecretID{FromString: "secretID"},
AppRole: "auth/approle",
IsWrappingToken: true,
},
false,
},
{
"fail with SecretID FromFail",
`{"RoleID": "roleID", "SecretID": {"FromFail": "secretID"}}`,
nil,
true,
},
{
"fail with SecretID empty FromEnv",
`{"RoleID": "roleID", "SecretID": {"FromEnv": ""}}`,
nil,
true,
},
{
"fail with SecretID empty FromFile",
`{"RoleID": "roleID", "SecretID": {"FromFile": ""}}`,
nil,
true,
},
{
"fail with SecretID empty FromString",
`{"RoleID": "roleID", "SecretID": {"FromString": ""}}`,
nil,
true,
},
{
"fail mandatory with SecretID FromFail",
`{"RoleID": "roleID", "SecretID": {"FromFail": "secretID"}}`,
nil,
true,
},
{
"fail missing RoleID",
`{"SecretID": {"FromString": "secretID"}}`,
nil,
true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

@ -51,10 +51,8 @@ type AuthDB interface {
Revoke(rci *RevokedCertificateInfo) error
RevokeSSH(rci *RevokedCertificateInfo) error
GetCertificate(serialNumber string) (*x509.Certificate, error)
StoreCertificate(crt *x509.Certificate) error
UseToken(id, tok string) (bool, error)
IsSSHHost(name string) (bool, error)
StoreSSHCertificate(crt *ssh.Certificate) error
GetSSHHostPrincipals() ([]string, error)
Shutdown() error
}
@ -82,6 +80,13 @@ func MustFromContext(ctx context.Context) AuthDB {
}
}
// CertificateStorer is an extension of AuthDB that allows to store
// certificates.
type CertificateStorer interface {
StoreCertificate(crt *x509.Certificate) error
StoreSSHCertificate(crt *ssh.Certificate) error
}
// DB is a wrapper over the nosql.DB interface.
type DB struct {
nosql.DB

@ -20,7 +20,7 @@ type SimpleDB struct {
usedTokens *sync.Map
}
func newSimpleDB(c *Config) (AuthDB, error) {
func newSimpleDB(c *Config) (*SimpleDB, error) {
db := &SimpleDB{}
db.usedTokens = new(sync.Map)
return db, nil

@ -654,7 +654,7 @@ preferably not all - meaning it never leaves the server on which it was created.
### Passwords
When you intialize your PKI (`step ca init`) the root and intermediate
When you initialize your PKI (`step ca init`) the root and intermediate
private keys will be encrypted with the same password. We recommend that you
change the password with which the intermediate is encrypted at your earliest
convenience.
@ -681,7 +681,7 @@ to divide the root private key password across a handful of trusted parties.
### Provisioners
When you intialize your PKI (`step ca init`) a default provisioner will be created
When you initialize your PKI (`step ca init`) a default provisioner will be created
and it's private key will be encrypted using the same password used to encrypt
the root private key. Before deploying the Step CA you should remove this
provisioner and add new ones that are encrypted with new, secure, random passwords.

@ -29,6 +29,7 @@ require (
github.com/googleapis/gax-go/v2 v2.1.1
github.com/hashicorp/vault/api v1.3.1
github.com/hashicorp/vault/api/auth/approle v0.1.1
github.com/hashicorp/vault/api/auth/kubernetes v0.1.0
github.com/jhump/protoreflect v1.9.0 // indirect
github.com/mattn/go-colorable v0.1.8 // indirect
github.com/mattn/go-isatty v0.0.13 // indirect
@ -46,7 +47,7 @@ require (
go.etcd.io/bbolt v1.3.6 // indirect
go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352
go.step.sm/cli-utils v0.7.0
go.step.sm/crypto v0.16.1
go.step.sm/crypto v0.16.2
go.step.sm/linkedca v0.16.1
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3
golang.org/x/net v0.0.0-20220403103023-749bd193bc2b
@ -64,3 +65,6 @@ require (
// replace go.step.sm/crypto => ../crypto
// replace go.step.sm/cli-utils => ../cli-utils
// replace go.step.sm/linkedca => ../linkedca
// use github.com/smallstep/pkcs7 fork with patches applied
replace go.mozilla.org/pkcs7 => github.com/smallstep/pkcs7 v0.0.0-20211016004704-52592125d6f6

@ -449,6 +449,8 @@ github.com/hashicorp/vault/api v1.3.1 h1:pkDkcgTh47PRjY1NEFeofqR4W/HkNUi9qIakESO
github.com/hashicorp/vault/api v1.3.1/go.mod h1:QeJoWxMFt+MsuWcYhmwRLwKEXrjwAFFywzhptMsTIUw=
github.com/hashicorp/vault/api/auth/approle v0.1.1 h1:R5yA+xcNvw1ix6bDuWOaLOq2L4L77zDCVsethNw97xQ=
github.com/hashicorp/vault/api/auth/approle v0.1.1/go.mod h1:mHOLgh//xDx4dpqXoq6tS8Ob0FoCFWLU2ibJ26Lfmag=
github.com/hashicorp/vault/api/auth/kubernetes v0.1.0 h1:6BtyahbF4aQp8gg3ww0A/oIoqzbhpNP1spXU3nHE0n0=
github.com/hashicorp/vault/api/auth/kubernetes v0.1.0/go.mod h1:Pdgk78uIs0mgDOLvc3a+h/vYIT9rznw2sz+ucuH9024=
github.com/hashicorp/vault/sdk v0.3.0 h1:kR3dpxNkhh/wr6ycaJYqp6AFT/i2xaftbfnwZduTKEY=
github.com/hashicorp/vault/sdk v0.3.0/go.mod h1:aZ3fNuL5VNydQk8GcLJ2TV8YCRVvyaakYkhZRoVuhj0=
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M=
@ -735,6 +737,8 @@ github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1
github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc=
github.com/smallstep/nosql v0.4.0 h1:Go3WYwttUuvwqMtFiiU4g7kBIlY+hR0bIZAqVdakQ3M=
github.com/smallstep/nosql v0.4.0/go.mod h1:yKZT5h7cdIVm6wEKM9+jN5dgK80Hljpuy8HNsnI7Gzo=
github.com/smallstep/pkcs7 v0.0.0-20211016004704-52592125d6f6 h1:8Rjy6IZbSM/jcYgBWCoLIGjug7QcoLtF9sUuhDrHD2U=
github.com/smallstep/pkcs7 v0.0.0-20211016004704-52592125d6f6/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
@ -796,9 +800,6 @@ go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=
go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
go.mozilla.org/pkcs7 v0.0.0-20210730143726-725912489c62/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk=
go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 h1:CCriYyAfq1Br1aIYettdHZTy8mBTIPo7We18TuO/bak=
go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk=
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
@ -813,10 +814,8 @@ go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqe
go.step.sm/cli-utils v0.7.0 h1:2GvY5Muid1yzp7YQbfCCS+gK3q7zlHjjLL5Z0DXz8ds=
go.step.sm/cli-utils v0.7.0/go.mod h1:Ur6bqA/yl636kCUJbp30J7Unv5JJ226eW2KqXPDwF/E=
go.step.sm/crypto v0.9.0/go.mod h1:+CYG05Mek1YDqi5WK0ERc6cOpKly2i/a5aZmU1sfGj0=
go.step.sm/crypto v0.16.1 h1:4mnZk21cSxyMGxsEpJwZKKvJvDu1PN09UVrWWFNUBdk=
go.step.sm/crypto v0.16.1/go.mod h1:3G0yQr5lQqfEG0CMYz8apC/qMtjLRQlzflL2AxkcN+g=
go.step.sm/linkedca v0.16.0 h1:9xdE150lRTEoBq1gJl+prePpSmNqXRXsez3qzRs3Lic=
go.step.sm/linkedca v0.16.0/go.mod h1:W59ucS4vFpuR0g4PtkGbbtXAwxbDEnNCg+ovkej1ANM=
go.step.sm/crypto v0.16.2 h1:Pr9aazTwWBBZNogUsOqhOrPSdwAa9pPs+lMB602lnDA=
go.step.sm/crypto v0.16.2/go.mod h1:1WkTOTY+fOX/RY4TnZREp6trQAsBHRQ7nu6QJBiNQF8=
go.step.sm/linkedca v0.16.1 h1:CdbMV5SjnlRsgeYTXaaZmQCkYIgJq8BOzpewri57M2k=
go.step.sm/linkedca v0.16.1/go.mod h1:W59ucS4vFpuR0g4PtkGbbtXAwxbDEnNCg+ovkej1ANM=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=

Loading…
Cancel
Save