From f868e07a763b70250cacbc637b8d86d3664f595e Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 5 Mar 2020 14:33:42 -0800 Subject: [PATCH] Allow to use custom principals on cloud provisioners. Fixes #203 --- authority/provisioner/aws.go | 21 ++++++++++++++------- authority/provisioner/aws_test.go | 23 +++++++++++++++++++---- authority/provisioner/azure.go | 10 ++++++++-- authority/provisioner/azure_test.go | 23 ++++++++++++++++++++--- authority/provisioner/gcp.go | 18 ++++++++++++------ authority/provisioner/gcp_test.go | 22 +++++++++++++++++++--- go.sum | 1 + 7 files changed, 93 insertions(+), 25 deletions(-) diff --git a/authority/provisioner/aws.go b/authority/provisioner/aws.go index 16820909..3f2c8873 100644 --- a/authority/provisioner/aws.go +++ b/authority/provisioner/aws.go @@ -449,18 +449,25 @@ func (p *AWS) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, doc := claims.document signOptions := []SignOption{ - // set the key id to the token subject - sshCertKeyIDModifier(claims.Subject), + // set the key id to the instance id + sshCertKeyIDModifier(doc.InstanceID), } - // Default to host + known IPs/hostnames - defaults := SSHOptions{ - CertType: SSHHostCert, - Principals: []string{ + // Only enforce known principals if disable custom sans is true. + var principals []string + if p.DisableCustomSANs { + principals = []string{ doc.PrivateIP, fmt.Sprintf("ip-%s.%s.compute.internal", strings.Replace(doc.PrivateIP, ".", "-", -1), doc.Region), - }, + } } + + // Default to cert type to host + defaults := SSHOptions{ + CertType: SSHHostCert, + Principals: principals, + } + // Validate user options signOptions = append(signOptions, sshCertOptionsValidator(defaults)) // Set defaults if not given as user options diff --git a/authority/provisioner/aws_test.go b/authority/provisioner/aws_test.go index 5e9ea92c..9e8fc7ad 100644 --- a/authority/provisioner/aws_test.go +++ b/authority/provisioner/aws_test.go @@ -582,17 +582,27 @@ func TestAWS_AuthorizeSSHSign(t *testing.T) { p1, srv, err := generateAWSWithServer() assert.FatalError(t, err) + p1.DisableCustomSANs = true defer srv.Close() p2, err := generateAWS() assert.FatalError(t, err) + p2.Accounts = p1.Accounts + p2.config = p1.config + p2.DisableCustomSANs = false + + p3, err := generateAWS() + assert.FatalError(t, err) // disable sshCA disable := false - p2.Claims = &Claims{EnableSSHCA: &disable} - p2.claimer, err = NewClaimer(p2.Claims, globalProvisionerClaims) + p3.Claims = &Claims{EnableSSHCA: &disable} + p3.claimer, err = NewClaimer(p3.Claims, globalProvisionerClaims) assert.FatalError(t, err) - t1, err := p1.GetIdentityToken("foo.local", "https://ca.smallstep.com") + t1, err := p1.GetIdentityToken("127.0.0.1", "https://ca.smallstep.com") + assert.FatalError(t, err) + + t2, err := p2.GetIdentityToken("foo.local", "https://ca.smallstep.com") assert.FatalError(t, err) key, err := generateJSONWebKey() @@ -620,6 +630,10 @@ func TestAWS_AuthorizeSSHSign(t *testing.T) { CertType: "host", Principals: []string{"ip-127-0-0-1.us-west-1.compute.internal"}, ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(hostDuration)), } + expectedCustomOptions := &SSHOptions{ + CertType: "host", Principals: []string{"foo.local"}, + ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(hostDuration)), + } type args struct { token string @@ -642,11 +656,12 @@ func TestAWS_AuthorizeSSHSign(t *testing.T) { {"ok-principal-ip", p1, args{t1, SSHOptions{Principals: []string{"127.0.0.1"}}, pub}, expectedHostOptionsIP, http.StatusOK, false, false}, {"ok-principal-hostname", p1, args{t1, SSHOptions{Principals: []string{"ip-127-0-0-1.us-west-1.compute.internal"}}, pub}, expectedHostOptionsHostname, http.StatusOK, false, false}, {"ok-options", p1, args{t1, SSHOptions{CertType: "host", Principals: []string{"127.0.0.1", "ip-127-0-0-1.us-west-1.compute.internal"}}, pub}, expectedHostOptions, http.StatusOK, false, false}, + {"ok-custom", p2, args{t2, SSHOptions{Principals: []string{"foo.local"}}, pub}, expectedCustomOptions, http.StatusOK, false, false}, {"fail-rsa1024", p1, args{t1, SSHOptions{}, rsa1024.Public()}, expectedHostOptions, http.StatusOK, false, true}, {"fail-type", p1, args{t1, SSHOptions{CertType: "user"}, pub}, nil, http.StatusOK, false, true}, {"fail-principal", p1, args{t1, SSHOptions{Principals: []string{"smallstep.com"}}, pub}, nil, http.StatusOK, false, true}, {"fail-extra-principal", p1, args{t1, SSHOptions{Principals: []string{"127.0.0.1", "ip-127-0-0-1.us-west-1.compute.internal", "smallstep.com"}}, pub}, nil, http.StatusOK, false, true}, - {"fail-sshCA-disabled", p2, args{"foo", SSHOptions{}, pub}, expectedHostOptions, http.StatusUnauthorized, true, false}, + {"fail-sshCA-disabled", p3, args{"foo", SSHOptions{}, pub}, expectedHostOptions, http.StatusUnauthorized, true, false}, {"fail-invalid-token", p1, args{"foo", SSHOptions{}, pub}, expectedHostOptions, http.StatusUnauthorized, true, false}, } for _, tt := range tests { diff --git a/authority/provisioner/azure.go b/authority/provisioner/azure.go index 88755c2a..5afe0472 100644 --- a/authority/provisioner/azure.go +++ b/authority/provisioner/azure.go @@ -318,14 +318,20 @@ func (p *Azure) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOptio return nil, errs.Wrap(http.StatusInternalServerError, err, "azure.AuthorizeSSHSign") } signOptions := []SignOption{ - // set the key id to the token subject + // set the key id to the instance name sshCertKeyIDModifier(name), } + // Only enforce known principals if disable custom sans is true. + var principals []string + if p.DisableCustomSANs { + principals = []string{name} + } + // Default to host + known hostnames defaults := SSHOptions{ CertType: SSHHostCert, - Principals: []string{name}, + Principals: principals, } // Validate user options signOptions = append(signOptions, sshCertOptionsValidator(defaults)) diff --git a/authority/provisioner/azure_test.go b/authority/provisioner/azure_test.go index f49624cc..6a226e09 100644 --- a/authority/provisioner/azure_test.go +++ b/authority/provisioner/azure_test.go @@ -505,19 +505,31 @@ func TestAzure_AuthorizeSSHSign(t *testing.T) { p1, srv, err := generateAzureWithServer() assert.FatalError(t, err) + p1.DisableCustomSANs = true defer srv.Close() p2, err := generateAzure() assert.FatalError(t, err) + p2.TenantID = p1.TenantID + p2.config = p1.config + p2.oidcConfig = p1.oidcConfig + p2.keyStore = p1.keyStore + p2.DisableCustomSANs = false + + p3, err := generateAzure() + assert.FatalError(t, err) // disable sshCA disable := false - p2.Claims = &Claims{EnableSSHCA: &disable} - p2.claimer, err = NewClaimer(p2.Claims, globalProvisionerClaims) + p3.Claims = &Claims{EnableSSHCA: &disable} + p3.claimer, err = NewClaimer(p3.Claims, globalProvisionerClaims) assert.FatalError(t, err) t1, err := p1.GetIdentityToken("subject", "caURL") assert.FatalError(t, err) + t2, err := p2.GetIdentityToken("subject", "caURL") + assert.FatalError(t, err) + key, err := generateJSONWebKey() assert.FatalError(t, err) @@ -535,6 +547,10 @@ func TestAzure_AuthorizeSSHSign(t *testing.T) { CertType: "host", Principals: []string{"virtualMachine"}, ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(hostDuration)), } + expectedCustomOptions := &SSHOptions{ + CertType: "host", Principals: []string{"foo.bar"}, + ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(hostDuration)), + } type args struct { token string @@ -555,11 +571,12 @@ func TestAzure_AuthorizeSSHSign(t *testing.T) { {"ok-type", p1, args{t1, SSHOptions{CertType: "host"}, pub}, expectedHostOptions, http.StatusOK, false, false}, {"ok-principals", p1, args{t1, SSHOptions{Principals: []string{"virtualMachine"}}, pub}, expectedHostOptions, http.StatusOK, false, false}, {"ok-options", p1, args{t1, SSHOptions{CertType: "host", Principals: []string{"virtualMachine"}}, pub}, expectedHostOptions, http.StatusOK, false, false}, + {"ok-custom", p2, args{t2, SSHOptions{Principals: []string{"foo.bar"}}, pub}, expectedCustomOptions, http.StatusOK, false, false}, {"fail-rsa1024", p1, args{t1, SSHOptions{}, rsa1024.Public()}, expectedHostOptions, http.StatusOK, false, true}, {"fail-type", p1, args{t1, SSHOptions{CertType: "user"}, pub}, nil, http.StatusOK, false, true}, {"fail-principal", p1, args{t1, SSHOptions{Principals: []string{"smallstep.com"}}, pub}, nil, http.StatusOK, false, true}, {"fail-extra-principal", p1, args{t1, SSHOptions{Principals: []string{"virtualMachine", "smallstep.com"}}, pub}, nil, http.StatusOK, false, true}, - {"fail-sshCA-disabled", p2, args{"foo", SSHOptions{}, pub}, expectedHostOptions, http.StatusUnauthorized, true, false}, + {"fail-sshCA-disabled", p3, args{"foo", SSHOptions{}, pub}, expectedHostOptions, http.StatusUnauthorized, true, false}, {"fail-invalid-token", p1, args{"foo", SSHOptions{}, pub}, expectedHostOptions, http.StatusUnauthorized, true, false}, } for _, tt := range tests { diff --git a/authority/provisioner/gcp.go b/authority/provisioner/gcp.go index d55b702f..28c7f51e 100644 --- a/authority/provisioner/gcp.go +++ b/authority/provisioner/gcp.go @@ -358,17 +358,23 @@ func (p *GCP) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, ce := claims.Google.ComputeEngine signOptions := []SignOption{ - // set the key id to the token subject + // set the key id to the instance name sshCertKeyIDModifier(ce.InstanceName), } + // Only enforce known principals if disable custom sans is true. + var principals []string + if p.DisableCustomSANs { + principals = []string{ + fmt.Sprintf("%s.c.%s.internal", ce.InstanceName, ce.ProjectID), + fmt.Sprintf("%s.%s.c.%s.internal", ce.InstanceName, ce.Zone, ce.ProjectID), + } + } + // Default to host + known hostnames defaults := SSHOptions{ - CertType: SSHHostCert, - Principals: []string{ - fmt.Sprintf("%s.c.%s.internal", ce.InstanceName, ce.ProjectID), - fmt.Sprintf("%s.%s.c.%s.internal", ce.InstanceName, ce.Zone, ce.ProjectID), - }, + CertType: SSHHostCert, + Principals: principals, } // Validate user options signOptions = append(signOptions, sshCertOptionsValidator(defaults)) diff --git a/authority/provisioner/gcp_test.go b/authority/provisioner/gcp_test.go index 0fbb4b41..34a09003 100644 --- a/authority/provisioner/gcp_test.go +++ b/authority/provisioner/gcp_test.go @@ -556,13 +556,18 @@ func TestGCP_AuthorizeSSHSign(t *testing.T) { p1, err := generateGCP() assert.FatalError(t, err) + p1.DisableCustomSANs = true p2, err := generateGCP() assert.FatalError(t, err) + p2.DisableCustomSANs = false + + p3, err := generateGCP() + assert.FatalError(t, err) // disable sshCA disable := false - p2.Claims = &Claims{EnableSSHCA: &disable} - p2.claimer, err = NewClaimer(p2.Claims, globalProvisionerClaims) + p3.Claims = &Claims{EnableSSHCA: &disable} + p3.claimer, err = NewClaimer(p3.Claims, globalProvisionerClaims) assert.FatalError(t, err) t1, err := generateGCPToken(p1.ServiceAccounts[0], @@ -571,6 +576,12 @@ func TestGCP_AuthorizeSSHSign(t *testing.T) { time.Now(), &p1.keyStore.keySet.Keys[0]) assert.FatalError(t, err) + t2, err := generateGCPToken(p2.ServiceAccounts[0], + "https://accounts.google.com", p2.GetID(), + "instance-id", "instance-name", "project-id", "zone", + time.Now(), &p2.keyStore.keySet.Keys[0]) + assert.FatalError(t, err) + key, err := generateJSONWebKey() assert.FatalError(t, err) @@ -596,6 +607,10 @@ func TestGCP_AuthorizeSSHSign(t *testing.T) { CertType: "host", Principals: []string{"instance-name.zone.c.project-id.internal"}, ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(hostDuration)), } + expectedCustomOptions := &SSHOptions{ + CertType: "host", Principals: []string{"foo.bar", "bar.foo"}, + ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(hostDuration)), + } type args struct { token string @@ -618,11 +633,12 @@ func TestGCP_AuthorizeSSHSign(t *testing.T) { {"ok-principal1", p1, args{t1, SSHOptions{Principals: []string{"instance-name.c.project-id.internal"}}, pub}, expectedHostOptionsPrincipal1, http.StatusOK, false, false}, {"ok-principal2", p1, args{t1, SSHOptions{Principals: []string{"instance-name.zone.c.project-id.internal"}}, pub}, expectedHostOptionsPrincipal2, http.StatusOK, false, false}, {"ok-options", p1, args{t1, SSHOptions{CertType: "host", Principals: []string{"instance-name.c.project-id.internal", "instance-name.zone.c.project-id.internal"}}, pub}, expectedHostOptions, http.StatusOK, false, false}, + {"ok-custom", p2, args{t2, SSHOptions{Principals: []string{"foo.bar", "bar.foo"}}, pub}, expectedCustomOptions, http.StatusOK, false, false}, {"fail-rsa1024", p1, args{t1, SSHOptions{}, rsa1024.Public()}, expectedHostOptions, http.StatusOK, false, true}, {"fail-type", p1, args{t1, SSHOptions{CertType: "user"}, pub}, nil, http.StatusOK, false, true}, {"fail-principal", p1, args{t1, SSHOptions{Principals: []string{"smallstep.com"}}, pub}, nil, http.StatusOK, false, true}, {"fail-extra-principal", p1, args{t1, SSHOptions{Principals: []string{"instance-name.c.project-id.internal", "instance-name.zone.c.project-id.internal", "smallstep.com"}}, pub}, nil, http.StatusOK, false, true}, - {"fail-sshCA-disabled", p2, args{"foo", SSHOptions{}, pub}, expectedHostOptions, http.StatusUnauthorized, true, false}, + {"fail-sshCA-disabled", p3, args{"foo", SSHOptions{}, pub}, expectedHostOptions, http.StatusUnauthorized, true, false}, {"fail-invalid-token", p1, args{"foo", SSHOptions{}, pub}, expectedHostOptions, http.StatusUnauthorized, true, false}, } for _, tt := range tests { diff --git a/go.sum b/go.sum index 5f1248ac..1a08d045 100644 --- a/go.sum +++ b/go.sum @@ -588,6 +588,7 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876 h1:sKJQZMuxjOAR/Uo2LBfU90onWEf1dF4C+0hPJCc9Mpc= golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 h1:xMPOj6Pz6UipU1wXLkrtqpHbR0AVFnyPEQq/wRWz9lM= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=