Skip to content

Commit

Permalink
Merge pull request #1075 from smallstep/herman/remote-management-helm
Browse files Browse the repository at this point in the history
Add `enableAdmin` and `enableACME` to Helm values.yml generation
  • Loading branch information
hslatman authored Oct 25, 2022
2 parents 2e39b63 + e90fe4b commit a718359
Show file tree
Hide file tree
Showing 14 changed files with 948 additions and 16 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- Added name constraints evaluation and enforcement when issuing or renewing
X.509 certificates.
- Added provisioner webhooks for augmenting template data and authorizing certificate requests before signing.
- Added automatic migration of provisioners when enabling remote managment.

### Fixed
- MySQL DSN parsing issues fixed with upgrade to [smallstep/[email protected]](https://github.com/smallstep/nosql/releases/tag/v0.5.0).
Expand Down
83 changes: 74 additions & 9 deletions authority/authority.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ type Authority struct {
sshCAUserFederatedCerts []ssh.PublicKey
sshCAHostFederatedCerts []ssh.PublicKey

// Do not re-initialize
// If true, do not re-initialize
initOnce bool
startTime time.Time

Expand All @@ -91,8 +91,11 @@ type Authority struct {

adminMutex sync.RWMutex

// Do Not initialize the authority
// If true, do not initialize the authority
skipInit bool

// If true, do not output initialization logs
quietInit bool
}

// Info contains information about the authority.
Expand Down Expand Up @@ -600,20 +603,74 @@ func (a *Authority) init() error {
return admin.WrapErrorISE(err, "error loading provisioners to initialize authority")
}
if len(provs) == 0 && !strings.EqualFold(a.config.AuthorityConfig.DeploymentType, "linked") {
// Create First Provisioner
prov, err := CreateFirstProvisioner(ctx, a.adminDB, string(a.password))
if err != nil {
return admin.WrapErrorISE(err, "error creating first provisioner")
// Migration will currently only be kicked off once, because either one or more provisioners
// are migrated or a default JWK provisioner will be created in the DB. It won't run for
// linked or hosted deployments. Not for linked, because that case is explicitly checked
// for above. Not for hosted, because there'll be at least an existing OIDC provisioner.
var firstJWKProvisioner *linkedca.Provisioner
if len(a.config.AuthorityConfig.Provisioners) > 0 {
// Existing provisioners detected; try migrating them to DB storage.
a.initLogf("Starting migration of provisioners")
for _, p := range a.config.AuthorityConfig.Provisioners {
lp, err := ProvisionerToLinkedca(p)
if err != nil {
return admin.WrapErrorISE(err, "error transforming provisioner %q while migrating", p.GetName())
}

// Store the provisioner to be migrated
if err := a.adminDB.CreateProvisioner(ctx, lp); err != nil {
return admin.WrapErrorISE(err, "error creating provisioner %q while migrating", p.GetName())
}

// Mark the first JWK provisioner, so that it can be used for administration purposes
if firstJWKProvisioner == nil && lp.Type == linkedca.Provisioner_JWK {
firstJWKProvisioner = lp
a.initLogf("Migrated JWK provisioner %q with admin permissions", p.GetName())
} else {
a.initLogf("Migrated %s provisioner %q", p.GetType(), p.GetName())
}
}

c := a.config
if c.WasLoadedFromFile() {
// The provisioners in the configuration file can be deleted from
// the file by editing it. Automatic rewriting of the file was considered
// to be too surprising for users and not the right solution for all
// use cases, so we leave it up to users to this themselves.
a.initLogf("Provisioners that were migrated can now be removed from `ca.json` by editing it")
}

a.initLogf("Finished migrating provisioners")
}

// Create first JWK provisioner for remote administration purposes if none exists yet
if firstJWKProvisioner == nil {
firstJWKProvisioner, err = CreateFirstProvisioner(ctx, a.adminDB, string(a.password))
if err != nil {
return admin.WrapErrorISE(err, "error creating first provisioner")
}
a.initLogf("Created JWK provisioner %q with admin permissions", firstJWKProvisioner.GetName())
}

// Create first admin
// Create first super admin, belonging to the first JWK provisioner
// TODO(hs): pass a user-provided first super admin subject to here. With `ca init` it's
// added to the DB immediately if using remote management. But when migrating from
// ca.json to the DB, this option doesn't exist. Adding a flag just to do it during
// migration isn't nice. We could opt for a user to change it afterwards. There exist
// cases in which creation of `step` could lock out a user from API access. This is the
// case if `step` isn't allowed to be signed by Name Constraints or the X.509 policy.
// We have protection for that when creating and updating a policy, but if a policy or
// Name Constraints are in use at the time of migration, that could lock the user out.
superAdminSubject := "step"
if err := a.adminDB.CreateAdmin(ctx, &linkedca.Admin{
ProvisionerId: prov.Id,
Subject: "step",
ProvisionerId: firstJWKProvisioner.Id,
Subject: superAdminSubject,
Type: linkedca.Admin_SUPER_ADMIN,
}); err != nil {
return admin.WrapErrorISE(err, "error creating first admin")
}

a.initLogf("Created super admin %q for JWK provisioner %q", superAdminSubject, firstJWKProvisioner.GetName())
}
}

Expand Down Expand Up @@ -663,6 +720,14 @@ func (a *Authority) init() error {
return nil
}

// initLogf is used to log initialization information. The output
// can be disabled by starting the CA with the `--quiet` flag.
func (a *Authority) initLogf(format string, v ...any) {
if !a.quietInit {
log.Printf(format, v...)
}
}

// GetID returns the define authority id or a zero uuid.
func (a *Authority) GetID() string {
const zeroUUID = "00000000-0000-0000-0000-000000000000"
Expand Down
31 changes: 31 additions & 0 deletions authority/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ type Config struct {
Templates *templates.Templates `json:"templates,omitempty"`
CommonName string `json:"commonName,omitempty"`
SkipValidation bool `json:"-"`

// Keeps record of the filename the Config is read from
loadedFromFilepath string
}

// ASN1DN contains ASN1.DN attributes that are used in Subject and Issuer
Expand Down Expand Up @@ -163,6 +166,10 @@ func LoadConfiguration(filename string) (*Config, error) {
return nil, errors.Wrapf(err, "error parsing %s", filename)
}

// store filename that was read to populate Config
c.loadedFromFilepath = filename

// initialize the Config
c.Init()

return &c, nil
Expand Down Expand Up @@ -199,6 +206,30 @@ func (c *Config) Save(filename string) error {
return errors.Wrapf(enc.Encode(c), "error writing %s", filename)
}

// Commit saves the current configuration to the same
// file it was initially loaded from.
//
// TODO(hs): rename Save() to WriteTo() and replace this
// with Save()? Or is Commit clear enough.
func (c *Config) Commit() error {
if !c.WasLoadedFromFile() {
return errors.New("cannot commit configuration if not loaded from file")
}
return c.Save(c.loadedFromFilepath)
}

// WasLoadedFromFile returns whether or not the Config was
// loaded from a file.
func (c *Config) WasLoadedFromFile() bool {
return c.loadedFromFilepath != ""
}

// Filepath returns the path to the file the Config was
// loaded from.
func (c *Config) Filepath() string {
return c.loadedFromFilepath
}

// Validate validates the configuration.
func (c *Config) Validate() error {
switch {
Expand Down
8 changes: 8 additions & 0 deletions authority/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,14 @@ func WithDatabase(d db.AuthDB) Option {
}
}

// WithQuietInit disables log output when the authority is initialized.
func WithQuietInit() Option {
return func(a *Authority) error {
a.quietInit = true
return nil
}
}

// WithWebhookClient sets the http.Client to be used for outbound requests.
func WithWebhookClient(c *http.Client) Option {
return func(a *Authority) error {
Expand Down
13 changes: 12 additions & 1 deletion ca/ca.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,10 @@ func (ca *CA) Init(cfg *config.Config) (*CA, error) {
opts = append(opts, authority.WithDatabase(ca.opts.database))
}

if ca.opts.quiet {
opts = append(opts, authority.WithQuietInit())
}

webhookTransport := http.DefaultTransport.(*http.Transport).Clone()
opts = append(opts, authority.WithWebhookClient(&http.Client{Transport: webhookTransport}))

Expand Down Expand Up @@ -345,7 +349,7 @@ func (ca *CA) Run() error {
if step.Contexts().GetCurrent() != nil {
log.Printf("Current context: %s", step.Contexts().GetCurrent().Name)
}
log.Printf("Config file: %s", ca.opts.configFile)
log.Printf("Config file: %s", ca.getConfigFileOutput())
baseURL := fmt.Sprintf("https://%s%s",
authorityInfo.DNSNames[0],
ca.config.Address[strings.LastIndex(ca.config.Address, ":"):])
Expand Down Expand Up @@ -565,3 +569,10 @@ func dumpRoutes(mux chi.Routes) {
fmt.Printf("Logging err: %s\n", err.Error())
}
}

func (ca *CA) getConfigFileOutput() string {
if ca.config.WasLoadedFromFile() {
return ca.config.Filepath()
}
return "loaded from token"
}
36 changes: 32 additions & 4 deletions pki/helm.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type helmVariables struct {
Defaults *linkedca.Defaults
Password string
EnableSSH bool
EnableAdmin bool
TLS authconfig.TLSOptions
Provisioners []provisioner.Interface
}
Expand All @@ -34,21 +35,47 @@ func (p *PKI) WriteHelmTemplate(w io.Writer) error {
p.Ssh = nil
}

// Convert provisioner to ca.json
provisioners := make([]provisioner.Interface, len(p.Authority.Provisioners))
for i, p := range p.Authority.Provisioners {
// Convert provisioners to ca.json representation
provisioners := []provisioner.Interface{}
for _, p := range p.Authority.Provisioners {
pp, err := authority.ProvisionerToCertificates(p)
if err != nil {
return err
}
provisioners[i] = pp
provisioners = append(provisioners, pp)
}

// Add default ACME provisioner if enabled. Note that this logic is similar
// to what's in p.GenerateConfig(), but that codepath isn't taken when
// writing the Helm template. The default JWK provisioner is added earlier in
// the process and that's part of the provisioners above.
// TODO(hs): consider refactoring the initialization, so that this becomes
// easier to reason about and maintain.
if p.options.enableACME {
provisioners = append(provisioners, &provisioner.ACME{
Type: "ACME",
Name: "acme",
})
}

// Add default SSHPOP provisioner if enabled. Similar to the above, this is
// the same as what happens in p.GenerateConfig().
if p.options.enableSSH {
provisioners = append(provisioners, &provisioner.SSHPOP{
Type: "SSHPOP",
Name: "sshpop",
Claims: &provisioner.Claims{
EnableSSHCA: &p.options.enableSSH,
},
})
}

if err := tmpl.Execute(w, helmVariables{
Configuration: &p.Configuration,
Defaults: &p.Defaults,
Password: "",
EnableSSH: p.options.enableSSH,
EnableAdmin: p.options.enableAdmin,
TLS: authconfig.DefaultTLSOptions,
Provisioners: provisioners,
}); err != nil {
Expand Down Expand Up @@ -88,6 +115,7 @@ inject:
type: badgerv2
dataSource: /home/step/db
authority:
enableAdmin: {{ .EnableAdmin }}
provisioners:
{{- range .Provisioners }}
- {{ . | toJson }}
Expand Down
Loading

0 comments on commit a718359

Please sign in to comment.