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 name: Documentation Request
about: Request documentation for a feature about: Request documentation for a feature
title: '' title: '[Docs]:'
labels: documentation, needs triage labels: docs, needs triage
assignees: '' 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. 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)? - Where would you like that documentation to live (command line usage output, website, github markdown on the repo)?

@ -1,13 +1,24 @@
--- ---
name: Enhancement name: Enhancement
about: Suggest an enhancement to step certificates about: Suggest an enhancement to step-ca
title: '' title: ''
labels: enhancement, needs triage labels: enhancement, needs triage
assignees: '' 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 uses: golangci/golangci-lint-action@v2
with: with:
# Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version # 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 # Optional: working directory, useful for monorepos
# working-directory: somedir # working-directory: somedir

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

@ -151,7 +151,7 @@ integration: bin/$(BINNAME)
######################################### #########################################
fmt: fmt:
$Q gofmt -l -w $(SRC) $Q gofmt -l -s -w $(SRC)
lint: lint:
$Q golangci-lint run --timeout=30m $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") return admin.WrapErrorISE(err, "error creating admin")
} }
if err := a.admins.Store(adm, prov); err != nil { 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 reloading admin resources on failed admin store")
} }
return admin.WrapErrorISE(err, "error storing admin in authority cache") 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) return nil, admin.WrapErrorISE(err, "error updating cached admin %s", id)
} }
if err := a.adminDB.UpdateAdmin(ctx, adm); err != nil { 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 reloading admin resources on failed admin update")
} }
return nil, admin.WrapErrorISE(err, "error updating admin %s", id) 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) return admin.WrapErrorISE(err, "error removing admin %s from authority cache", id)
} }
if err := a.adminDB.DeleteAdmin(ctx, id); err != nil { 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 reloading admin resources on failed admin remove")
} }
return admin.WrapErrorISE(err, "error deleting admin %s", id) return admin.WrapErrorISE(err, "error deleting admin %s", id)

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

@ -1034,7 +1034,7 @@ func TestAuthority_authorizeSSHSign(t *testing.T) {
} }
} else { } else {
if assert.Nil(t, tc.err) { 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:]...), PemCertificateChain: serializeCertificateChain(fullchain[1:]...),
PemParentCertificate: serializeCertificateChain(parent), 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) ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel() defer cancel()
_, err := c.client.PostSSHCertificate(ctx, &linkedca.SSHCertificateRequest{ _, err := c.client.PostSSHCertificate(ctx, &linkedca.SSHCertificateRequest{
Certificate: string(ssh.MarshalAuthorizedKey(crt)), Certificate: string(ssh.MarshalAuthorizedKey(crt)),
Provisioner: createProvisionerIdentity(prov),
}) })
return errors.Wrap(err, "error posting ssh certificate") 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 { func (c *linkedCaClient) Revoke(crt *x509.Certificate, rci *db.RevokedCertificateInfo) error {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel() 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 // WithLinkedCAToken is an option to set the authentication token used to enable
// linked ca. // linked ca.
func WithLinkedCAToken(token string) Option { 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) { func readCertificateBundle(pemCerts []byte) ([]*x509.Certificate, error) {
var block *pem.Block var block *pem.Block
var certs []*x509.Certificate var certs []*x509.Certificate

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

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

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

@ -3,6 +3,7 @@ package provisioner
import ( import (
"context" "context"
"crypto/x509" "crypto/x509"
"net/http"
"regexp" "regexp"
"strings" "strings"
"time" "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)) 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() { 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 return nil

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

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

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

@ -368,9 +368,10 @@ func TestK8sSA_AuthorizeSSHSign(t *testing.T) {
} else { } else {
if assert.Nil(t, tc.err) { if assert.Nil(t, tc.err) {
if assert.NotNil(t, opts) { if assert.NotNil(t, opts) {
assert.Len(t, 7, opts) assert.Len(t, 8, opts)
for _, o := range opts { for _, o := range opts {
switch v := o.(type) { switch v := o.(type) {
case Interface:
case sshCertificateOptionsFunc: case sshCertificateOptionsFunc:
case *sshCertOptionsRequireValidator: case *sshCertOptionsRequireValidator:
assert.Equals(t, v, &sshCertOptionsRequireValidator{CertType: true, KeyID: true, Principals: true}) 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, return append(signOptions,
p,
templateOptions, templateOptions,
// Checks the validity bounds, and set the validity if has not been set. // Checks the validity bounds, and set the validity if has not been set.
&sshLimitDuration{p.ctl.Claimer, crt.Details.NotAfter}, &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) { 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) { 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, return append(signOptions,
o,
// Set the validity bounds if not set. // Set the validity bounds if not set.
&sshDefaultDuration{o.ctl.Claimer}, &sshDefaultDuration{o.ctl.Claimer},
// Validate public key // Validate public key

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

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

@ -769,6 +769,7 @@ func TestX5C_AuthorizeSSHSign(t *testing.T) {
nw := now() nw := now()
for _, o := range opts { for _, o := range opts {
switch v := o.(type) { switch v := o.(type) {
case Interface:
case sshCertOptionsValidator: case sshCertOptionsValidator:
tc.claims.Step.SSH.ValidAfter.t = time.Time{} tc.claims.Step.SSH.ValidAfter.t = time.Time{}
tc.claims.Step.SSH.ValidBefore.t = time.Time{} tc.claims.Step.SSH.ValidBefore.t = time.Time{}
@ -799,9 +800,9 @@ func TestX5C_AuthorizeSSHSign(t *testing.T) {
tot++ tot++
} }
if len(tc.claims.Step.SSH.CertType) > 0 { if len(tc.claims.Step.SSH.CertType) > 0 {
assert.Equals(t, tot, 10) assert.Equals(t, tot, 11)
} else { } 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 { func (a *Authority) StoreProvisioner(ctx context.Context, prov *linkedca.Provisioner) error {
a.adminMutex.Lock() a.adminMutex.Lock()
defer a.adminMutex.Unlock() 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.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 reloading admin resources on failed provisioner store")
} }
return admin.WrapErrorISE(err, "error storing provisioner in authority cache") 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) 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.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 reloading admin resources on failed provisioner update")
} }
return admin.WrapErrorISE(err, "error updating provisioner '%s'", nu.Name) 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() provName, provID := p.GetName(), p.GetID()
// Validate if a.IsAdminAPIEnabled() {
// - Check that there will be SUPER_ADMINs that remain after we // Validate
// remove this provisioner. // - Check that there will be SUPER_ADMINs that remain after we
if a.admins.SuperCount() == a.admins.SuperCountByProvisioner(provName) { // remove this provisioner.
return admin.NewError(admin.ErrorBadRequestType, if a.IsAdminAPIEnabled() && a.admins.SuperCount() == a.admins.SuperCountByProvisioner(provName) {
"cannot remove provisioner %s because no super admins will remain", provName) return admin.NewError(admin.ErrorBadRequestType,
} "cannot remove provisioner %s because no super admins will remain", provName)
}
// Delete all admins associated with the provisioner. // Delete all admins associated with the provisioner.
admins, ok := a.admins.LoadByProvisioner(provName) admins, ok := a.admins.LoadByProvisioner(provName)
if ok { if ok {
for _, adm := range admins { for _, adm := range admins {
if err := a.removeAdmin(ctx, adm.Id); err != nil { 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) return admin.WrapErrorISE(err, "error deleting admin %s, as part of provisioner %s deletion", adm.Subject, provName)
}
} }
} }
} }
// Remove provisioner from authority caches. // Remove provisioner from authority caches.
if err := a.provisioners.Remove(provID); err != nil { 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. // Remove provisioner from database.
if err := a.adminDB.DeleteProvisioner(ctx, provID); err != nil { 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 reloading admin resources on failed provisioner remove")
} }
return admin.WrapErrorISE(err, "error deleting provisioner %s", provName) 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 // Set backdate with the configured value
opts.Backdate = a.config.AuthorityConfig.Backdate.Duration opts.Backdate = a.config.AuthorityConfig.Backdate.Duration
var prov provisioner.Interface
for _, op := range signOpts { for _, op := range signOpts {
switch o := op.(type) { switch o := op.(type) {
// Capture current provisioner
case provisioner.Interface:
prov = o
// add options to NewCertificate // add options to NewCertificate
case provisioner.SSHCertificateOptions: case provisioner.SSHCertificateOptions:
certOptions = append(certOptions, o.Options(opts)...) 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") 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") 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") 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 nil, errs.Wrap(http.StatusInternalServerError, err, "rekeySSH; error storing certificate in db")
} }
return cert, nil 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 { 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 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 // 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 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") 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` instead of just
// `StoreCertificate(*x509.Certificate) error`. // `StoreCertificate(*x509.Certificate) error`.
func (a *Authority) storeCertificate(prov provisioner.Interface, fullchain []*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 StoreCertificateChain(provisioner.Interface, ...*x509.Certificate) error
} }
type certificateChainStorer interface { type certificateChainSimpleStorer interface {
StoreCertificateChain(...*x509.Certificate) error StoreCertificateChain(...*x509.Certificate) error
} }
// Store certificate in linkedca // Store certificate in linkedca
switch s := a.adminDB.(type) { switch s := a.adminDB.(type) {
case linkedChainStorer:
return s.StoreCertificateChain(prov, fullchain...)
case certificateChainStorer: case certificateChainStorer:
return s.StoreCertificateChain(prov, fullchain...)
case certificateChainSimpleStorer:
return s.StoreCertificateChain(fullchain...) return s.StoreCertificateChain(fullchain...)
} }
// Store certificate in local db // Store certificate in local db
switch s := a.db.(type) { switch s := a.db.(type) {
case linkedChainStorer:
return s.StoreCertificateChain(prov, fullchain...)
case certificateChainStorer: case certificateChainStorer:
return s.StoreCertificateChain(prov, fullchain...)
case certificateChainSimpleStorer:
return s.StoreCertificateChain(fullchain...) return s.StoreCertificateChain(fullchain...)
case db.CertificateStorer:
return s.StoreCertificate(fullchain[0])
default: default:
return a.db.StoreCertificate(fullchain[0]) return nil
} }
} }
@ -398,15 +401,21 @@ func (a *Authority) storeRenewedCertificate(oldCert *x509.Certificate, fullchain
type renewedCertificateChainStorer interface { type renewedCertificateChainStorer interface {
StoreRenewedCertificate(*x509.Certificate, ...*x509.Certificate) error StoreRenewedCertificate(*x509.Certificate, ...*x509.Certificate) error
} }
// Store certificate in linkedca // Store certificate in linkedca
if s, ok := a.adminDB.(renewedCertificateChainStorer); ok { if s, ok := a.adminDB.(renewedCertificateChainStorer); ok {
return s.StoreRenewedCertificate(oldCert, fullchain...) return s.StoreRenewedCertificate(oldCert, fullchain...)
} }
// Store certificate in local db // Store certificate in local db
if s, ok := a.db.(renewedCertificateChainStorer); ok { switch s := a.db.(type) {
case renewedCertificateChainStorer:
return s.StoreRenewedCertificate(oldCert, fullchain...) 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. // RevokeOptions are the options for the Revoke API.

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

@ -7,6 +7,7 @@ import (
"net" "net"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"os"
"reflect" "reflect"
"strings" "strings"
"sync" "sync"
@ -374,6 +375,9 @@ func TestBootstrapClient(t *testing.T) {
} }
func TestBootstrapClientServerRotation(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) reset := setMinCertDuration(1 * time.Second)
defer reset() defer reset()

@ -425,16 +425,18 @@ func parseEndpoint(endpoint string) (*url.URL, error) {
} }
// ProvisionerOption is the type of options passed to the Provisioner method. // 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 { // ProvisionerOptions stores options for the provisioner CRUD API.
cursor string type ProvisionerOptions struct {
limit int Cursor string
id string Limit int
name string 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 { for _, fn := range opts {
if err = fn(o); err != nil { if err = fn(o); err != nil {
return return
@ -443,51 +445,51 @@ func (o *provisionerOptions) apply(opts []ProvisionerOption) (err error) {
return return
} }
func (o *provisionerOptions) rawQuery() string { func (o *ProvisionerOptions) rawQuery() string {
v := url.Values{} v := url.Values{}
if len(o.cursor) > 0 { if o.Cursor != "" {
v.Set("cursor", o.cursor) v.Set("cursor", o.Cursor)
} }
if o.limit > 0 { if o.Limit > 0 {
v.Set("limit", strconv.Itoa(o.limit)) v.Set("limit", strconv.Itoa(o.Limit))
} }
if len(o.id) > 0 { if o.ID != "" {
v.Set("id", o.id) v.Set("id", o.ID)
} }
if len(o.name) > 0 { if o.Name != "" {
v.Set("name", o.name) v.Set("name", o.Name)
} }
return v.Encode() return v.Encode()
} }
// WithProvisionerCursor will request the provisioners starting with the given cursor. // WithProvisionerCursor will request the provisioners starting with the given cursor.
func WithProvisionerCursor(cursor string) ProvisionerOption { func WithProvisionerCursor(cursor string) ProvisionerOption {
return func(o *provisionerOptions) error { return func(o *ProvisionerOptions) error {
o.cursor = cursor o.Cursor = cursor
return nil return nil
} }
} }
// WithProvisionerLimit will request the given number of provisioners. // WithProvisionerLimit will request the given number of provisioners.
func WithProvisionerLimit(limit int) ProvisionerOption { func WithProvisionerLimit(limit int) ProvisionerOption {
return func(o *provisionerOptions) error { return func(o *ProvisionerOptions) error {
o.limit = limit o.Limit = limit
return nil return nil
} }
} }
// WithProvisionerID will request the given provisioner. // WithProvisionerID will request the given provisioner.
func WithProvisionerID(id string) ProvisionerOption { func WithProvisionerID(id string) ProvisionerOption {
return func(o *provisionerOptions) error { return func(o *ProvisionerOptions) error {
o.id = id o.ID = id
return nil return nil
} }
} }
// WithProvisionerName will request the given provisioner. // WithProvisionerName will request the given provisioner.
func WithProvisionerName(name string) ProvisionerOption { func WithProvisionerName(name string) ProvisionerOption {
return func(o *provisionerOptions) error { return func(o *ProvisionerOptions) error {
o.name = name o.Name = name
return nil return nil
} }
} }
@ -810,8 +812,8 @@ retry:
// paginate the provisioners. // paginate the provisioners.
func (c *Client) Provisioners(opts ...ProvisionerOption) (*api.ProvisionersResponse, error) { func (c *Client) Provisioners(opts ...ProvisionerOption) (*api.ProvisionersResponse, error) {
var retried bool var retried bool
o := new(provisionerOptions) o := new(ProvisionerOptions)
if err := o.apply(opts); err != nil { if err := o.Apply(opts); err != nil {
return nil, err return nil, err
} }
u := c.endpoint.ResolveReference(&url.URL{ 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" "time"
"github.com/smallstep/certificates/cas/apiv1" "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" vault "github.com/hashicorp/vault/api"
auth "github.com/hashicorp/vault/api/auth/approle"
) )
func init() { func init() {
@ -29,15 +30,14 @@ func init() {
// VaultOptions defines the configuration options added using the // VaultOptions defines the configuration options added using the
// apiv1.Options.Config field. // apiv1.Options.Config field.
type VaultOptions struct { type VaultOptions struct {
PKI string `json:"pki,omitempty"` PKIMountPath string `json:"pkiMountPath,omitempty"`
PKIRoleDefault string `json:"pkiRoleDefault,omitempty"` PKIRoleDefault string `json:"pkiRoleDefault,omitempty"`
PKIRoleRSA string `json:"pkiRoleRSA,omitempty"` PKIRoleRSA string `json:"pkiRoleRSA,omitempty"`
PKIRoleEC string `json:"pkiRoleEC,omitempty"` PKIRoleEC string `json:"pkiRoleEC,omitempty"`
PKIRoleEd25519 string `json:"pkiRoleEd25519,omitempty"` PKIRoleEd25519 string `json:"pkiRoleEd25519,omitempty"`
RoleID string `json:"roleID,omitempty"` AuthType string `json:"authType,omitempty"`
SecretID auth.SecretID `json:"secretID,omitempty"` AuthMountPath string `json:"authMountPath,omitempty"`
AppRole string `json:"appRole,omitempty"` AuthOptions json.RawMessage `json:"authOptions,omitempty"`
IsWrappingToken bool `json:"isWrappingToken,omitempty"`
} }
// VaultCAS implements a Certificate Authority Service using Hashicorp Vault. // 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) return nil, fmt.Errorf("unable to initialize vault client: %w", err)
} }
var appRoleAuth *auth.AppRoleAuth var method vault.AuthMethod
if vc.IsWrappingToken { switch vc.AuthType {
appRoleAuth, err = auth.NewAppRoleAuth( case "kubernetes":
vc.RoleID, method, err = kubernetes.NewKubernetesAuthMethod(vc.AuthMountPath, vc.AuthOptions)
&vc.SecretID, case "approle":
auth.WithWrappingToken(), method, err = approle.NewApproleAuthMethod(vc.AuthMountPath, vc.AuthOptions)
auth.WithMountPath(vc.AppRole), default:
) return nil, fmt.Errorf("unknown auth type: %s, only 'kubernetes' and 'approle' currently supported", vc.AuthType)
} else {
appRoleAuth, err = auth.NewAppRoleAuth(
vc.RoleID,
&vc.SecretID,
auth.WithMountPath(vc.AppRole),
)
} }
if err != nil { 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 { 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 { if authInfo == nil {
return nil, errors.New("no auth info was returned after login") 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 // GetCertificateAuthority returns the root certificate of the certificate
// authority using the configured fingerprint. // authority using the configured fingerprint.
func (v *VaultCAS) GetCertificateAuthority(req *apiv1.GetCertificateAuthorityRequest) (*apiv1.GetCertificateAuthorityResponse, error) { 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 { if err != nil {
return nil, fmt.Errorf("error reading ca chain: %w", err) 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{}{ vaultReq := map[string]interface{}{
"serial_number": formatSerialNumber(sn), "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 { if err != nil {
return nil, fmt.Errorf("error revoking certificate: %w", err) 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(), "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 { if err != nil {
return nil, nil, fmt.Errorf("error signing certificate: %w", err) 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) { func loadOptions(config json.RawMessage) (*VaultOptions, error) {
var vc *VaultOptions // setup default values
vc := VaultOptions{
PKIMountPath: "pki",
PKIRoleDefault: "default",
}
err := json.Unmarshal(config, &vc) err := json.Unmarshal(config, &vc)
if err != nil { if err != nil {
return nil, fmt.Errorf("error decoding vaultCAS config: %w", err) 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 == "" { if vc.PKIRoleRSA == "" {
vc.PKIRoleRSA = vc.PKIRoleDefault vc.PKIRoleRSA = vc.PKIRoleDefault
} }
@ -272,23 +262,7 @@ func loadOptions(config json.RawMessage) (*VaultOptions, error) {
vc.PKIRoleEd25519 = vc.PKIRoleDefault vc.PKIRoleEd25519 = vc.PKIRoleDefault
} }
if vc.RoleID == "" { return &vc, nil
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
} }
func parseCertificates(pemCert string) []*x509.Certificate { func parseCertificates(pemCert string) []*x509.Certificate {

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

@ -51,10 +51,8 @@ type AuthDB interface {
Revoke(rci *RevokedCertificateInfo) error Revoke(rci *RevokedCertificateInfo) error
RevokeSSH(rci *RevokedCertificateInfo) error RevokeSSH(rci *RevokedCertificateInfo) error
GetCertificate(serialNumber string) (*x509.Certificate, error) GetCertificate(serialNumber string) (*x509.Certificate, error)
StoreCertificate(crt *x509.Certificate) error
UseToken(id, tok string) (bool, error) UseToken(id, tok string) (bool, error)
IsSSHHost(name string) (bool, error) IsSSHHost(name string) (bool, error)
StoreSSHCertificate(crt *ssh.Certificate) error
GetSSHHostPrincipals() ([]string, error) GetSSHHostPrincipals() ([]string, error)
Shutdown() 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. // DB is a wrapper over the nosql.DB interface.
type DB struct { type DB struct {
nosql.DB nosql.DB

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

@ -654,7 +654,7 @@ preferably not all - meaning it never leaves the server on which it was created.
### Passwords ### 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 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 change the password with which the intermediate is encrypted at your earliest
convenience. convenience.
@ -681,7 +681,7 @@ to divide the root private key password across a handful of trusted parties.
### Provisioners ### 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 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 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. 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/googleapis/gax-go/v2 v2.1.1
github.com/hashicorp/vault/api v1.3.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/approle v0.1.1
github.com/hashicorp/vault/api/auth/kubernetes v0.1.0
github.com/jhump/protoreflect v1.9.0 // indirect github.com/jhump/protoreflect v1.9.0 // indirect
github.com/mattn/go-colorable v0.1.8 // indirect github.com/mattn/go-colorable v0.1.8 // indirect
github.com/mattn/go-isatty v0.0.13 // indirect github.com/mattn/go-isatty v0.0.13 // indirect
@ -46,7 +47,7 @@ require (
go.etcd.io/bbolt v1.3.6 // indirect go.etcd.io/bbolt v1.3.6 // indirect
go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352
go.step.sm/cli-utils v0.7.0 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 go.step.sm/linkedca v0.16.1
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3
golang.org/x/net v0.0.0-20220403103023-749bd193bc2b golang.org/x/net v0.0.0-20220403103023-749bd193bc2b
@ -64,3 +65,6 @@ require (
// replace go.step.sm/crypto => ../crypto // replace go.step.sm/crypto => ../crypto
// replace go.step.sm/cli-utils => ../cli-utils // replace go.step.sm/cli-utils => ../cli-utils
// replace go.step.sm/linkedca => ../linkedca // 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 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 h1:R5yA+xcNvw1ix6bDuWOaLOq2L4L77zDCVsethNw97xQ=
github.com/hashicorp/vault/api/auth/approle v0.1.1/go.mod h1:mHOLgh//xDx4dpqXoq6tS8Ob0FoCFWLU2ibJ26Lfmag= 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 h1:kR3dpxNkhh/wr6ycaJYqp6AFT/i2xaftbfnwZduTKEY=
github.com/hashicorp/vault/sdk v0.3.0/go.mod h1:aZ3fNuL5VNydQk8GcLJ2TV8YCRVvyaakYkhZRoVuhj0= 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= 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/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 h1:Go3WYwttUuvwqMtFiiU4g7kBIlY+hR0bIZAqVdakQ3M=
github.com/smallstep/nosql v0.4.0/go.mod h1:yKZT5h7cdIVm6wEKM9+jN5dgK80Hljpuy8HNsnI7Gzo= 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/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/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= 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 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=
go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= 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.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.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.20.2/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= 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 h1:2GvY5Muid1yzp7YQbfCCS+gK3q7zlHjjLL5Z0DXz8ds=
go.step.sm/cli-utils v0.7.0/go.mod h1:Ur6bqA/yl636kCUJbp30J7Unv5JJ226eW2KqXPDwF/E= 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.9.0/go.mod h1:+CYG05Mek1YDqi5WK0ERc6cOpKly2i/a5aZmU1sfGj0=
go.step.sm/crypto v0.16.1 h1:4mnZk21cSxyMGxsEpJwZKKvJvDu1PN09UVrWWFNUBdk= go.step.sm/crypto v0.16.2 h1:Pr9aazTwWBBZNogUsOqhOrPSdwAa9pPs+lMB602lnDA=
go.step.sm/crypto v0.16.1/go.mod h1:3G0yQr5lQqfEG0CMYz8apC/qMtjLRQlzflL2AxkcN+g= go.step.sm/crypto v0.16.2/go.mod h1:1WkTOTY+fOX/RY4TnZREp6trQAsBHRQ7nu6QJBiNQF8=
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/linkedca v0.16.1 h1:CdbMV5SjnlRsgeYTXaaZmQCkYIgJq8BOzpewri57M2k= go.step.sm/linkedca v0.16.1 h1:CdbMV5SjnlRsgeYTXaaZmQCkYIgJq8BOzpewri57M2k=
go.step.sm/linkedca v0.16.1/go.mod h1:W59ucS4vFpuR0g4PtkGbbtXAwxbDEnNCg+ovkej1ANM= 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= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=

Loading…
Cancel
Save