Skip to content

Commit

Permalink
Merge pull request #1976 from leonweecs/leonweecs/vault-aws-auth
Browse files Browse the repository at this point in the history
Add AWS auth method for Vault RA mode
  • Loading branch information
hslatman committed Sep 12, 2024
2 parents 5b1eebd + 78e7678 commit c118a2a
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 c118a2a

Please sign in to comment.