MWI: Fix missing CA chain in AWS Roles Anywhere exchange (#54884)

* Fix handling of chain

* Add test coverage for external PKI and AWS Roles Anywhere

* Follow Edoardo's advice re buffer init

* unfallible buffer write
This commit is contained in:
Noah Stride
2025-05-20 08:11:21 +00:00
committed by GitHub
parent 0a4d35ff68
commit 5fd40eba4b
4 changed files with 173 additions and 109 deletions
+8 -1
View File
@@ -91,7 +91,14 @@ func (s *WorkloadIdentityAWSRAService) generate(ctx context.Context) error {
if err != nil {
return trace.Wrap(err, "marshaling private key")
}
svid, err := x509svid.ParseRaw(res.GetX509Svid().Cert, pkcs8)
certWithChain := new(bytes.Buffer)
_, _ = certWithChain.Write(res.GetX509Svid().GetCert())
// If external PKI is configured, we need to append the chain to the leaf
// certificate before calling x509svid.ParseRaw.
for _, cert := range res.GetX509Svid().GetChain() {
_, _ = certWithChain.Write(cert)
}
svid, err := x509svid.ParseRaw(certWithChain.Bytes(), pkcs8)
if err != nil {
return trace.Wrap(err, "parsing x509 svid")
}
+160 -108
View File
@@ -36,6 +36,7 @@ import (
"github.com/gravitational/teleport/api/types"
apiutils "github.com/gravitational/teleport/api/utils"
"github.com/gravitational/teleport/lib/tbot/config"
"github.com/gravitational/teleport/lib/tlsca"
"github.com/gravitational/teleport/lib/utils"
"github.com/gravitational/teleport/lib/utils/testutils/golden"
"github.com/gravitational/teleport/tool/teleport/testenv"
@@ -145,48 +146,97 @@ func TestBotWorkloadIdentityAWSRA(t *testing.T) {
ctx := context.Background()
log := utils.NewSlogLoggerForTests()
process := testenv.MakeTestServer(t, defaultTestServerOpts(t, log))
rootClient := testenv.MakeDefaultAuthClient(t, process)
tests := []struct {
name string
externalPKI bool
}{
{
name: "no external pki",
},
{
name: "external pki",
externalPKI: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
process := testenv.MakeTestServer(t, defaultTestServerOpts(t, log))
if tt.externalPKI {
setWorkloadIdentityX509CAOverride(ctx, t, process)
}
spiffeCA, err := process.GetAuthServer().
GetCertAuthority(ctx, types.CertAuthID{
DomainName: "root",
Type: types.SPIFFECA,
}, false)
require.NoError(t, err)
spiffeCAX509KeyPairs := spiffeCA.GetTrustedTLSKeyPairs()
require.Len(t, spiffeCAX509KeyPairs, 1)
spiffeCACert, err := tlsca.ParseCertificatePEM(spiffeCAX509KeyPairs[0].Cert)
require.NoError(t, err)
rootClient := testenv.MakeDefaultAuthClient(t, process)
roleArn := "arn:aws:iam::123456789012:role/example-role"
trustAnchorArn := "arn:aws:rolesanywhere:us-east-1:123456789012:trust-anchor/0000000-0000-0000-0000-000000000000"
profileArn := "arn:aws:rolesanywhere:us-east-1:123456789012:profile/0000000-0000-0000-0000-00000000000"
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/sessions", r.URL.Path)
assert.Equal(t, http.MethodPost, r.Method)
roleArn := "arn:aws:iam::123456789012:role/example-role"
trustAnchorArn := "arn:aws:rolesanywhere:us-east-1:123456789012:trust-anchor/0000000-0000-0000-0000-000000000000"
profileArn := "arn:aws:rolesanywhere:us-east-1:123456789012:profile/0000000-0000-0000-0000-00000000000"
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/sessions", r.URL.Path)
assert.Equal(t, http.MethodPost, r.Method)
// Check query parameter inputs
// The AWS documentation "lies" about these inputs using the JSON body
// - the rolesanywhere API client in
// `aws/rolesanywhere-credential-helper` uses query parameters for
// these.
assert.Equal(t, roleArn, r.URL.Query().Get("roleArn"))
assert.Equal(t, trustAnchorArn, r.URL.Query().Get("trustAnchorArn"))
assert.Equal(t, profileArn, r.URL.Query().Get("profileArn"))
// Check query parameter inputs
// The AWS documentation "lies" about these inputs using the JSON body
// - the rolesanywhere API client in
// `aws/rolesanywhere-credential-helper` uses query parameters for
// these.
assert.Equal(t, roleArn, r.URL.Query().Get("roleArn"))
assert.Equal(t, trustAnchorArn, r.URL.Query().Get("trustAnchorArn"))
assert.Equal(t, profileArn, r.URL.Query().Get("profileArn"))
// Check JSON body inputs
body := &mockCreateSessionInputBody{}
assert.NoError(t, json.NewDecoder(r.Body).Decode(body))
assert.Equal(t, int((2 * time.Hour).Seconds()), body.DurationSeconds)
// Check JSON body inputs
body := &mockCreateSessionInputBody{}
assert.NoError(t, json.NewDecoder(r.Body).Decode(body))
assert.Equal(t, int((2 * time.Hour).Seconds()), body.DurationSeconds)
// Validate the X-Amz-X509 header contains the valid (and correct) SVID
derString := r.Header.Get("X-Amz-X509")
assert.NotEmpty(t, derString)
derBytes, err := base64.StdEncoding.DecodeString(derString)
assert.NoError(t, err)
cert, err := x509.ParseCertificate(derBytes)
assert.NoError(t, err)
assert.Len(t, cert.URIs, 1)
assert.Equal(t, "spiffe://root/ra-test", cert.URIs[0].String())
// Validate the X-Amz-X509 header contains the valid (and correct) SVID
derString := r.Header.Get("X-Amz-X509")
assert.NotEmpty(t, derString)
derBytes, err := base64.StdEncoding.DecodeString(derString)
assert.NoError(t, err)
cert, err := x509.ParseCertificate(derBytes)
assert.NoError(t, err)
assert.Len(t, cert.URIs, 1)
assert.Equal(t, "spiffe://root/ra-test", cert.URIs[0].String())
// Validate the authorization header exists. We rely on the AWS SDK to
// actually produce the signature, and, validating this signature would
// introduce significant complexity to this test - so this is omitted.
authz := r.Header.Get("Authorization")
assert.NotEmpty(t, authz)
// Validate the X-Amz-X509-Chain header contains the valid chain
chainString := r.Header.Get("X-Amz-X509-Chain")
if tt.externalPKI {
require.NotEmpty(t, chainString)
// If there were multiple certs in the chain, we'd need to
// split by comma first since:
//
// > The X-Amz-X509-Chain header MUST be encoded as
// > comma-delimited, base64-encoded DER
//
// But since we only expect a single item in the chain here
// we can just decode it.
chainBytes, err := base64.StdEncoding.DecodeString(chainString)
assert.NoError(t, err)
chainCert, err := x509.ParseCertificate(chainBytes)
assert.NoError(t, err)
// Check this matches the actual CA we setup.
assert.True(t, chainCert.Equal(spiffeCACert))
} else {
require.Empty(t, chainString)
}
// Send mocked response
_, _ = w.Write([]byte(`{
// Validate the authorization header exists. We rely on the AWS SDK to
// actually produce the signature, and, validating this signature would
// introduce significant complexity to this test - so this is omitted.
authz := r.Header.Get("Authorization")
assert.NotEmpty(t, authz)
// Send mocked response
_, _ = w.Write([]byte(`{
"credentialSet":[
{
"assumedRoleUser": {
@@ -206,81 +256,83 @@ func TestBotWorkloadIdentityAWSRA(t *testing.T) {
],
"subjectArn": "arn:aws:rolesanywhere:us-east-1:000000000000:subject/41cl0bae-6783-40d4-ab20-65dc5d922e45"
}`))
}))
t.Cleanup(srv.Close)
}))
t.Cleanup(srv.Close)
role, err := types.NewRole("issue-foo", types.RoleSpecV6{
Allow: types.RoleConditions{
WorkloadIdentityLabels: map[string]apiutils.Strings{
"foo": []string{"bar"},
},
Rules: []types.Rule{
{
Resources: []string{types.KindWorkloadIdentity},
Verbs: []string{types.VerbRead, types.VerbList},
role, err := types.NewRole("issue-foo", types.RoleSpecV6{
Allow: types.RoleConditions{
WorkloadIdentityLabels: map[string]apiutils.Strings{
"foo": []string{"bar"},
},
Rules: []types.Rule{
{
Resources: []string{types.KindWorkloadIdentity},
Verbs: []string{types.VerbRead, types.VerbList},
},
},
},
},
},
})
require.NoError(t, err)
role, err = rootClient.UpsertRole(ctx, role)
require.NoError(t, err)
})
require.NoError(t, err)
role, err = rootClient.UpsertRole(ctx, role)
require.NoError(t, err)
workloadIdentity := &workloadidentityv1pb.WorkloadIdentity{
Kind: types.KindWorkloadIdentity,
Version: types.V1,
Metadata: &headerv1.Metadata{
Name: "foo-bar-bizz",
Labels: map[string]string{
"foo": "bar",
},
},
Spec: &workloadidentityv1pb.WorkloadIdentitySpec{
Spiffe: &workloadidentityv1pb.WorkloadIdentitySPIFFE{
Id: "/ra-test",
},
},
}
workloadIdentity, err = rootClient.WorkloadIdentityResourceServiceClient().
CreateWorkloadIdentity(ctx, &workloadidentityv1pb.CreateWorkloadIdentityRequest{
WorkloadIdentity: workloadIdentity,
workloadIdentity := &workloadidentityv1pb.WorkloadIdentity{
Kind: types.KindWorkloadIdentity,
Version: types.V1,
Metadata: &headerv1.Metadata{
Name: "foo-bar-bizz",
Labels: map[string]string{
"foo": "bar",
},
},
Spec: &workloadidentityv1pb.WorkloadIdentitySpec{
Spiffe: &workloadidentityv1pb.WorkloadIdentitySPIFFE{
Id: "/ra-test",
},
},
}
workloadIdentity, err = rootClient.WorkloadIdentityResourceServiceClient().
CreateWorkloadIdentity(ctx, &workloadidentityv1pb.CreateWorkloadIdentityRequest{
WorkloadIdentity: workloadIdentity,
})
require.NoError(t, err)
tmpDir := t.TempDir()
onboarding, _ := makeBot(t, rootClient, "ra-test", role.GetName())
botConfig := defaultBotConfig(t, process, onboarding, config.ServiceConfigs{
&config.WorkloadIdentityAWSRAService{
Selector: config.WorkloadIdentitySelector{
Name: workloadIdentity.GetMetadata().GetName(),
},
Destination: &config.DestinationDirectory{
Path: tmpDir,
},
RoleARN: roleArn,
ProfileARN: profileArn,
TrustAnchorARN: trustAnchorArn,
Region: "us-east-1",
SessionDuration: 2 * time.Hour,
SessionRenewalInterval: 30 * time.Minute,
EndpointOverride: srv.URL,
},
}, defaultBotConfigOpts{
useAuthServer: true,
insecure: true,
})
botConfig.Oneshot = true
b := New(botConfig, log)
// Run Bot with 10 second timeout to catch hangs.
ctx, cancel := context.WithTimeout(ctx, time.Second*10)
t.Cleanup(cancel)
require.NoError(t, b.Run(ctx))
got, err := os.ReadFile(filepath.Join(tmpDir, "aws_credentials"))
require.NoError(t, err)
if golden.ShouldSet() {
golden.Set(t, got)
}
require.Equal(t, string(golden.Get(t)), string(got))
})
require.NoError(t, err)
tmpDir := t.TempDir()
onboarding, _ := makeBot(t, rootClient, "ra-test", role.GetName())
botConfig := defaultBotConfig(t, process, onboarding, config.ServiceConfigs{
&config.WorkloadIdentityAWSRAService{
Selector: config.WorkloadIdentitySelector{
Name: workloadIdentity.GetMetadata().GetName(),
},
Destination: &config.DestinationDirectory{
Path: tmpDir,
},
RoleARN: roleArn,
ProfileARN: profileArn,
TrustAnchorARN: trustAnchorArn,
Region: "us-east-1",
SessionDuration: 2 * time.Hour,
SessionRenewalInterval: 30 * time.Minute,
EndpointOverride: srv.URL,
},
}, defaultBotConfigOpts{
useAuthServer: true,
insecure: true,
})
botConfig.Oneshot = true
b := New(botConfig, log)
// Run Bot with 10 second timeout to catch hangs.
ctx, cancel := context.WithTimeout(ctx, time.Second*10)
t.Cleanup(cancel)
require.NoError(t, b.Run(ctx))
got, err := os.ReadFile(filepath.Join(tmpDir, "aws_credentials"))
require.NoError(t, err)
if golden.ShouldSet() {
golden.Set(t, got)
}
require.Equal(t, string(golden.Get(t)), string(got))
}
@@ -0,0 +1,5 @@
[default]
aws_secret_access_key=secretAccessKey
aws_access_key_id=accessKeyId
aws_session_token=sessionToken
expiration=1848285415000