Add `--admin-subject` flag to `ca init`

The first super admin subject can now be provided through the
`--admin-subject` flag when initializing a CA.

It's not yet possible to configure the subject of the first
super admin when provisioners are migrated from `ca.json` to the
database. This effectively limits usage of the flag to scenarios
in which the provisioners are written to the database immediately,
so when `--remote-management` is enabled. It currently also doesn't
work with Helm deployments, because there's no mechanism yet to
pass this type of option to the Helm chart.

This commit partially addresses https://github.com/smallstep/cli/issues/697
pull/1075/head
Herman Slatman 2 years ago
parent 57001168a5
commit d981b9e0dc
No known key found for this signature in database
GPG Key ID: F4D8A44EA0A75A4F

@ -663,6 +663,14 @@ func (a *Authority) init() error {
} }
// Create first super admin, belonging to the first JWK provisioner // 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.
firstSuperAdminSubject := "step" firstSuperAdminSubject := "step"
if err := a.adminDB.CreateAdmin(ctx, &linkedca.Admin{ if err := a.adminDB.CreateAdmin(ctx, &linkedca.Admin{
ProvisionerId: firstJWKProvisioner.Id, ProvisionerId: firstJWKProvisioner.Id,

@ -21,148 +21,125 @@ import (
) )
func TestPKI_WriteHelmTemplate(t *testing.T) { func TestPKI_WriteHelmTemplate(t *testing.T) {
type fields struct { var preparePKI = func(t *testing.T, opts ...Option) *PKI {
casOptions apiv1.Options o := apiv1.Options{
pkiOptions []Option Type: "softcas",
IsCreator: true,
}
// Add default WithHelm option
opts = append(opts, WithHelm())
// TODO(hs): invoking `New` doesn't perform all operations that are executed
// when `ca init --helm` is executed. Ideally this logic should be handled
// in one place and probably inside of the PKI initialization. For testing
// purposes the missing operations to fill a Helm template fully are faked
// by `setKeyPair`, `setCertificates` and `setSSHSigningKeys`
p, err := New(o, opts...)
assert.NoError(t, err)
// setKeyPair sets a predefined JWK and a default JWK provisioner. This is one
// of the things performed in the `ca init` code that's not part of `New`, but
// performed after that in p.GenerateKeyPairs`. We're currently using the same
// JWK for every test to keep test variance small: we're not testing JWK generation
// here after all. It's a bit dangerous to redefine the function here, but it's
// the simplest way to make this fully testable without refactoring the init now.
// The password for the predefined encrypted key is \x01\x03\x03\x07.
setKeyPair(t, p)
// setCertificates sets some static intermediate and root CA certificate bytes. It
// replaces the logic executed in `p.GenerateRootCertificate`, `p.WriteRootCertificate`,
// and `p.GenerateIntermediateCertificate`.
setCertificates(t, p)
// setSSHSigningKeys sets predefined SSH user and host certificate and key bytes.
// This replaces the logic in `p.GenerateSSHSigningKeys`
setSSHSigningKeys(t, p)
return p
} }
tests := []struct { type test struct {
name string pki *PKI
fields fields
testFile string testFile string
wantErr bool wantErr bool
}{ }
{ var tests = map[string]func(t *testing.T) test{
name: "ok/simple", "ok/simple": func(t *testing.T) test {
fields: fields{ return test{
pkiOptions: []Option{ pki: preparePKI(t),
WithHelm(), testFile: "testdata/helm/simple.yml",
}, wantErr: false,
casOptions: apiv1.Options{ }
Type: "softcas",
IsCreator: true,
},
},
testFile: "testdata/helm/simple.yml",
wantErr: false,
}, },
{ "ok/with-provisioner": func(t *testing.T) test {
name: "ok/with-provisioner", return test{
fields: fields{ pki: preparePKI(t, WithProvisioner("a-provisioner")),
pkiOptions: []Option{ testFile: "testdata/helm/with-provisioner.yml",
WithHelm(), wantErr: false,
WithProvisioner("a-provisioner"), }
},
casOptions: apiv1.Options{
Type: "softcas",
IsCreator: true,
},
},
testFile: "testdata/helm/with-provisioner.yml",
wantErr: false,
}, },
{ "ok/with-acme": func(t *testing.T) test {
name: "ok/with-acme", return test{
fields: fields{ pki: preparePKI(t, WithACME()),
pkiOptions: []Option{ testFile: "testdata/helm/with-acme.yml",
WithHelm(), wantErr: false,
WithACME(), }
},
casOptions: apiv1.Options{
Type: "softcas",
IsCreator: true,
},
},
testFile: "testdata/helm/with-acme.yml",
wantErr: false,
}, },
{ "ok/with-admin": func(t *testing.T) test {
name: "ok/with-admin", return test{
fields: fields{ pki: preparePKI(t, WithAdmin()),
pkiOptions: []Option{ testFile: "testdata/helm/with-admin.yml",
WithHelm(), wantErr: false,
WithAdmin(), }
},
casOptions: apiv1.Options{
Type: "softcas",
IsCreator: true,
},
},
testFile: "testdata/helm/with-admin.yml",
wantErr: false,
}, },
{ "ok/with-ssh": func(t *testing.T) test {
name: "ok/with-ssh", return test{
fields: fields{ pki: preparePKI(t, WithSSH()),
pkiOptions: []Option{ testFile: "testdata/helm/with-ssh.yml",
WithHelm(), wantErr: false,
WithSSH(), }
},
casOptions: apiv1.Options{
Type: "softcas",
IsCreator: true,
},
},
testFile: "testdata/helm/with-ssh.yml",
wantErr: false,
}, },
{ "ok/with-ssh-and-acme": func(t *testing.T) test {
name: "ok/with-ssh-and-acme", return test{
fields: fields{ pki: preparePKI(t, WithSSH(), WithACME()),
pkiOptions: []Option{ testFile: "testdata/helm/with-ssh-and-acme.yml",
WithHelm(), wantErr: false,
WithACME(), }
WithSSH(), },
}, "fail/authority.ProvisionerToCertificates": func(t *testing.T) test {
casOptions: apiv1.Options{ pki := preparePKI(t)
Type: "softcas", pki.Authority.Provisioners = append(pki.Authority.Provisioners,
IsCreator: true, &linkedca.Provisioner{
Type: linkedca.Provisioner_JWK,
Name: "Broken JWK",
Details: nil,
}, },
}, )
testFile: "testdata/helm/with-ssh-and-acme.yml", return test{
wantErr: false, pki: pki,
wantErr: true,
}
}, },
} }
for _, tt := range tests { for name, run := range tests {
t.Run(tt.name, func(t *testing.T) { tc := run(t)
o := tt.fields.casOptions t.Run(name, func(t *testing.T) {
opts := tt.fields.pkiOptions
// TODO(hs): invoking `New` doesn't perform all operations that are executed
// when `ca init --helm` is executed. Ideally this logic should be handled
// in one place and probably inside of the PKI initialization. For testing
// purposes the missing operations to fill a Helm template fully are faked
// by `setKeyPair`, `setCertificates` and `setSSHSigningKeys`
p, err := New(o, opts...)
assert.NoError(t, err)
// setKeyPair sets a predefined JWK and a default JWK provisioner. This is one
// of the things performed in the `ca init` code that's not part of `New`, but
// performed after that in p.GenerateKeyPairs`. We're currently using the same
// JWK for every test to keep test variance small: we're not testing JWK generation
// here after all. It's a bit dangerous to redefine the function here, but it's
// the simplest way to make this fully testable without refactoring the init now.
// The password for the predefined encrypted key is \x01\x03\x03\x07.
setKeyPair(t, p)
// setCertificates sets some static intermediate and root CA certificate bytes. It
// replaces the logic executed in `p.GenerateRootCertificate`, `p.WriteRootCertificate`,
// and `p.GenerateIntermediateCertificate`.
setCertificates(t, p)
// setSSHSigningKeys sets predefined SSH user and host certificate and key bytes.
// This replaces the logic in `p.GenerateSSHSigningKeys`
setSSHSigningKeys(t, p)
w := &bytes.Buffer{} w := &bytes.Buffer{}
if err := p.WriteHelmTemplate(w); (err != nil) != tt.wantErr { if err := tc.pki.WriteHelmTemplate(w); (err != nil) != tc.wantErr {
t.Errorf("PKI.WriteHelmTemplate() error = %v, wantErr %v", err, tt.wantErr) t.Errorf("PKI.WriteHelmTemplate() error = %v, wantErr %v", err, tc.wantErr)
return
}
if tc.wantErr {
// don't compare output if an error was expected on output
return return
} }
wantBytes, err := os.ReadFile(tt.testFile) wantBytes, err := os.ReadFile(tc.testFile)
assert.NoError(t, err) assert.NoError(t, err)
if diff := cmp.Diff(wantBytes, w.Bytes()); diff != "" { if diff := cmp.Diff(wantBytes, w.Bytes()); diff != "" {
t.Logf("Generated Helm template did not match reference %q\n", tt.testFile) t.Logf("Generated Helm template did not match reference %q\n", tc.testFile)
t.Errorf("Diff follows:\n%s\n", diff) t.Errorf("Diff follows:\n%s\n", diff)
t.Errorf("Full output:\n%s\n", w.Bytes()) t.Errorf("Full output:\n%s\n", w.Bytes())
} }

@ -175,18 +175,19 @@ func GetProvisionerKey(caURL, rootFile, kid string) (string, error) {
} }
type options struct { type options struct {
provisioner string provisioner string
pkiOnly bool firstSuperAdminSubject string
enableACME bool pkiOnly bool
enableSSH bool enableACME bool
enableAdmin bool enableSSH bool
noDB bool enableAdmin bool
isHelm bool noDB bool
deploymentType DeploymentType isHelm bool
rootKeyURI string deploymentType DeploymentType
intermediateKeyURI string rootKeyURI string
hostKeyURI string intermediateKeyURI string
userKeyURI string hostKeyURI string
userKeyURI string
} }
// Option is the type of a configuration option on the pki constructor. // Option is the type of a configuration option on the pki constructor.
@ -220,6 +221,15 @@ func WithProvisioner(s string) Option {
} }
} }
// WithFirstSuperAdminSubject defines the subject of the first
// super admin for use with the Admin API. The admin will belong
// to the first JWK provisioner.
func WithFirstSuperAdminSubject(s string) Option {
return func(p *PKI) {
p.options.firstSuperAdminSubject = s
}
}
// WithPKIOnly will only generate the PKI without the step-ca config files. // WithPKIOnly will only generate the PKI without the step-ca config files.
func WithPKIOnly() Option { func WithPKIOnly() Option {
return func(p *PKI) { return func(p *PKI) {
@ -886,6 +896,11 @@ func (p *PKI) GenerateConfig(opt ...ConfigOption) (*authconfig.Config, error) {
// //
// Note that we might want to be able to define the database as a // Note that we might want to be able to define the database as a
// flag in `step ca init` so we can write to the proper place. // flag in `step ca init` so we can write to the proper place.
//
// TODO(hs): the logic for creating the provisioners and the super admin
// is similar to what's done when automatically migrating the provisioners.
// This is related to the existing comment above. Refactor this to exist in
// a single place and ensure it happensonly once.
_db, err := db.New(cfg.DB) _db, err := db.New(cfg.DB)
if err != nil { if err != nil {
return nil, err return nil, err
@ -909,9 +924,13 @@ func (p *PKI) GenerateConfig(opt ...ConfigOption) (*authconfig.Config, error) {
} }
} }
// Add the first provisioner as an admin. // Add the first provisioner as an admin.
firstSuperAdminSubject := "step"
if p.options.firstSuperAdminSubject != "" {
firstSuperAdminSubject = p.options.firstSuperAdminSubject
}
if err := adminDB.CreateAdmin(context.Background(), &linkedca.Admin{ if err := adminDB.CreateAdmin(context.Background(), &linkedca.Admin{
AuthorityId: admin.DefaultAuthorityID, AuthorityId: admin.DefaultAuthorityID,
Subject: "step", Subject: firstSuperAdminSubject,
Type: linkedca.Admin_SUPER_ADMIN, Type: linkedca.Admin_SUPER_ADMIN,
ProvisionerId: adminID, ProvisionerId: adminID,
}); err != nil { }); err != nil {

Loading…
Cancel
Save