Skip to content

Commit

Permalink
Add AWS auth method for Vault RA mode
Browse files Browse the repository at this point in the history
This commit adds the AWS auth method for Vault RA mode following similar
pattern in the existing approle and kubernetes methods.

This auth method supports both iam and ec2 auth type, see
https://developer.hashicorp.com/vault/docs/auth/aws for more info.

Implements #1946
  • Loading branch information
leonweecs committed Sep 12, 2024
1 parent 5b1eebd commit 78e7678
Show file tree
Hide file tree
Showing 5 changed files with 326 additions and 0 deletions.
84 changes: 84 additions & 0 deletions cas/vaultcas/auth/aws/aws.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package aws

import (
"encoding/json"
"fmt"

"github.com/hashicorp/vault/api/auth/aws"
)

// AuthOptions defines the configuration options added using the
// VaultOptions.AuthOptions field when AuthType is aws.
// This maps directly to Vault's AWS Login options,
// see: https://developer.hashicorp.com/vault/api-docs/auth/aws#login
type AuthOptions struct {
Role string `json:"role,omitempty"`
Region string `json:"region,omitempty"`
AwsAuthType string `json:"awsAuthType,omitempty"`

// options specific to 'iam' auth type
IamServerIDHeader string `json:"iamServerIdHeader"`

// options specific to 'ec2' auth type
SignatureType string `json:"signatureType,omitempty"`
Nonce string `json:"nonce,omitempty"`
}

func NewAwsAuthMethod(mountPath string, options json.RawMessage) (*aws.AWSAuth, error) {
var opts *AuthOptions

err := json.Unmarshal(options, &opts)
if err != nil {
return nil, fmt.Errorf("error decoding AWS auth options: %w", err)
}

var awsAuth *aws.AWSAuth

var loginOptions []aws.LoginOption
if mountPath != "" {
loginOptions = append(loginOptions, aws.WithMountPath(mountPath))
}
if opts.Role != "" {
loginOptions = append(loginOptions, aws.WithRole(opts.Role))
}
if opts.Region != "" {
loginOptions = append(loginOptions, aws.WithRegion(opts.Region))
}

switch opts.AwsAuthType {
case "iam":
loginOptions = append(loginOptions, aws.WithIAMAuth())

if opts.IamServerIDHeader != "" {
loginOptions = append(loginOptions, aws.WithIAMServerIDHeader(opts.IamServerIDHeader))
}
case "ec2":
loginOptions = append(loginOptions, aws.WithEC2Auth())

switch opts.SignatureType {
case "pkcs7":
loginOptions = append(loginOptions, aws.WithPKCS7Signature())
case "identity":
loginOptions = append(loginOptions, aws.WithIdentitySignature())
case "rsa2048":
loginOptions = append(loginOptions, aws.WithRSA2048Signature())
case "":
// no-op
default:
return nil, fmt.Errorf("unknown SignatureType type %q; valid options are 'pkcs7', 'identity' and 'rsa2048'", opts.SignatureType)
}

if opts.Nonce != "" {
loginOptions = append(loginOptions, aws.WithNonce(opts.Nonce))
}
default:
return nil, fmt.Errorf("unknown awsAuthType %q; valid options are 'iam' and 'ec2'", opts.AwsAuthType)
}

awsAuth, err = aws.NewAWSAuth(loginOptions...)
if err != nil {
return nil, fmt.Errorf("unable to initialize AWS auth method: %w", err)
}

return awsAuth, nil
}
189 changes: 189 additions & 0 deletions cas/vaultcas/auth/aws/aws_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
package aws

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/aws/login":
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `{
"auth": {
"client_token": "hvs.0000"
}
}`)
case r.RequestURI == "/v1/auth/custom-aws/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 TestAws_LoginMountPaths(t *testing.T) {
_, client := testCAHelper(t)

// Dummy AWS credentials is needed for Vault client to sign the STS request
t.Setenv("AWS_ACCESS_KEY_ID", "AKIAIOSFODNN7EXAMPLE")
t.Setenv("AWS_SECRET_ACCESS_KEY", "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY")

tests := []struct {
name string
mountPath string
token string
}{
{
name: "ok default mount path",
mountPath: "",
token: "hvs.0000",
},
{
name: "ok explicit mount path",
mountPath: "aws",
token: "hvs.0000",
},
{
name: "ok custom mount path",
mountPath: "custom-aws",
token: "hvs.9999",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
method, err := NewAwsAuthMethod(tt.mountPath, json.RawMessage(`{"role":"test-role","awsAuthType":"iam"}`))
if err != nil {
t.Errorf("NewAwsAuthMethod() 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 TestAws_NewAwsAuthMethod(t *testing.T) {
tests := []struct {
name string
mountPath string
raw string
wantErr bool
}{
{
"ok iam",
"",
`{"role":"test-role","awsAuthType":"iam"}`,
false,
},
{
"ok iam with region",
"",
`{"role":"test-role","awsAuthType":"iam","region":"us-east-1"}`,
false,
},
{
"ok iam with header",
"",
`{"role":"test-role","awsAuthType":"iam","iamServerIdHeader":"vault.example.com"}`,
false,
},
{
"ok ec2",
"",
`{"role":"test-role","awsAuthType":"ec2"}`,
false,
},
{
"ok ec2 with nonce",
"",
`{"role":"test-role","awsAuthType":"ec2","nonce": "0000-0000-0000-0000"}`,
false,
},
{
"ok ec2 with signature type",
"",
`{"role":"test-role","awsAuthType":"ec2","signatureType":"rsa2048"}`,
false,
},
{
"fail mandatory role",
"",
`{}`,
true,
},
{
"fail mandatory auth type",
"",
`{"role":"test-role"}`,
true,
},
{
"fail invalid auth type",
"",
`{"role":"test-role","awsAuthType":"test"}`,
true,
},
{
"fail invalid ec2 signature type",
"",
`{"role":"test-role","awsAuthType":"test","signatureType":"test"}`,
true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := NewAwsAuthMethod(tt.mountPath, json.RawMessage(tt.raw))
if (err != nil) != tt.wantErr {
t.Errorf("Aws.NewAwsAuthMethod() error = %v, wantErr %v", err, tt.wantErr)
return
}
})
}
}
3 changes: 3 additions & 0 deletions cas/vaultcas/vaultcas.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (

"github.com/smallstep/certificates/cas/apiv1"
"github.com/smallstep/certificates/cas/vaultcas/auth/approle"
"github.com/smallstep/certificates/cas/vaultcas/auth/aws"
"github.com/smallstep/certificates/cas/vaultcas/auth/kubernetes"

vault "github.com/hashicorp/vault/api"
Expand Down Expand Up @@ -84,6 +85,8 @@ func New(ctx context.Context, opts apiv1.Options) (*VaultCAS, error) {
method, err = kubernetes.NewKubernetesAuthMethod(vc.AuthMountPath, vc.AuthOptions)
case "approle":
method, err = approle.NewApproleAuthMethod(vc.AuthMountPath, vc.AuthOptions)
case "aws":
method, err = aws.NewAwsAuthMethod(vc.AuthMountPath, vc.AuthOptions)
default:
return nil, fmt.Errorf("unknown auth type: %s, only 'kubernetes' and 'approle' currently supported", vc.AuthType)
}
Expand Down
7 changes: 7 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ require (
github.com/googleapis/gax-go/v2 v2.13.0
github.com/hashicorp/vault/api v1.14.0
github.com/hashicorp/vault/api/auth/approle v0.7.0
github.com/hashicorp/vault/api/auth/aws v0.7.0
github.com/hashicorp/vault/api/auth/kubernetes v0.7.0
github.com/newrelic/go-agent/v3 v3.34.0
github.com/pkg/errors v0.9.1
Expand Down Expand Up @@ -63,6 +64,7 @@ require (
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.3.0 // indirect
github.com/ThalesIgnite/crypto11 v1.2.5 // indirect
github.com/aws/aws-sdk-go v1.49.22 // indirect
github.com/aws/aws-sdk-go-v2 v1.30.4 // indirect
github.com/aws/aws-sdk-go-v2/config v1.27.31 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.30 // indirect
Expand All @@ -87,6 +89,7 @@ require (
github.com/dgraph-io/ristretto v0.1.0 // indirect
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fatih/color v1.16.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-jose/go-jose/v4 v4.0.2 // indirect
github.com/go-kit/kit v0.13.0 // indirect
Expand All @@ -109,18 +112,22 @@ require (
github.com/googleapis/enterprise-certificate-proxy v0.3.3 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-hclog v1.6.3 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
github.com/hashicorp/go-secure-stdlib/awsutil v0.1.6 // indirect
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 // indirect
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect
github.com/hashicorp/go-sockaddr v1.0.2 // indirect
github.com/hashicorp/go-uuid v1.0.2 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/huandu/xstrings v1.5.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.6.0 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/manifoldco/promptui v0.9.0 // indirect
Expand Down
Loading

0 comments on commit 78e7678

Please sign in to comment.