-
Notifications
You must be signed in to change notification settings - Fork 1
/
aws_sts_assume_role.go
163 lines (139 loc) · 5.13 KB
/
aws_sts_assume_role.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
package provider
import (
"context"
"fmt"
"net"
"net/url"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/sts"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/jetstack/spiffe-connector/internal/pkg/server/proto"
)
// AWSSTSAssumeRoleProviderOptions are the options available to configure a AWSSTSAssumeRoleProvider
type AWSSTSAssumeRoleProviderOptions struct {
// Endpoint is passed to the session to select with AWS endpoint to use, this is optional
Endpoint string
// Region will be used if endpoint is set, defaults to us-east-1
Region string
// Duration is how long credentials will be valid for, recommended max: 1hr. Durations greater than 1hr might be
// blocked by organisation settings.
Duration int64
// CredentialsOverride will use explicit credentials if set, rather than letting the AWS SDK discover them
CredentialsOverride *credentials.Credentials
}
// AWSSTSAssumeRoleProvider is a provider used to get short lived credentials from AWS STS
type AWSSTSAssumeRoleProvider struct {
pingHost string
stsService *sts.STS
duration int64
}
// NewAWSSTSAssumeRoleProvider will configure a new AWSSTSAssumeRoleProvider using the supplied options
func NewAWSSTSAssumeRoleProvider(ctx context.Context, options AWSSTSAssumeRoleProviderOptions) (AWSSTSAssumeRoleProvider, error) {
// from https://docs.aws.amazon.com/STS/latest/APIReference/welcome.html
pingHost := "sts.amazonaws.com:https"
var config aws.Config
if options.Endpoint != "" {
ep, err := url.Parse(options.Endpoint)
if err != nil {
return AWSSTSAssumeRoleProvider{}, fmt.Errorf("failed to parse supplied endpoint: %w", err)
}
if ep.Scheme != "https" && ep.Scheme != "http" {
return AWSSTSAssumeRoleProvider{}, fmt.Errorf("supplied endpoint value should have http(s) scheme: %q", options.Endpoint)
}
if ep.Host == "" {
return AWSSTSAssumeRoleProvider{}, fmt.Errorf("supplied endpoint value should have host set")
}
if ep.Path != "" {
return AWSSTSAssumeRoleProvider{}, fmt.Errorf("supplied endpoint value should not have path set")
}
// pingHost is set to the Host of the validated URL, this will contain a port if one was set
pingHost = ep.Host
// if there is not a port set in the supplied endpoint, then we get the net package to dial on http or https
// based on the scheme
if ep.Port() == "" {
pingHost = fmt.Sprintf("%s:http", ep.Host)
if ep.Scheme == "https" {
pingHost = fmt.Sprintf("%s:https", ep.Host)
}
}
if options.Region == "" {
// non empty region is needed if setting endpoint
options.Region = "us-east-1"
}
config.Endpoint = &options.Endpoint
config.Region = &options.Region
}
if options.CredentialsOverride != nil {
config.Credentials = options.CredentialsOverride
}
sess, err := session.NewSession(&config)
if err != nil {
if aerr, ok := err.(awserr.Error); ok {
return AWSSTSAssumeRoleProvider{}, fmt.Errorf("failed to create session: %s: %s", aerr.Code(), aerr.Message())
}
return AWSSTSAssumeRoleProvider{}, fmt.Errorf("failed to create session: %w", err)
}
duration := int64(60 * 60)
if options.Duration > 0 {
duration = options.Duration
}
return AWSSTSAssumeRoleProvider{
stsService: sts.New(sess),
pingHost: pingHost,
duration: duration,
}, nil
}
// Name returns the name of the provider
func (p *AWSSTSAssumeRoleProvider) Name() string {
return "AWSSTSAssumeRoleProvider"
}
// Ping tests the configured credential providing endpoint is reachable
// Note: this does not test AWS authn/authz
func (p *AWSSTSAssumeRoleProvider) Ping() error {
_, err := net.DialTimeout("tcp", p.pingHost, time.Second*3)
if err != nil {
return fmt.Errorf("provider ping failed: %w", err)
}
return nil
}
// GetCredential will use STS to get a short lived credential for the given objectReference (Role)
// spiffe-connector must be able to AssumeRole for the supplied role for this to work
func (p *AWSSTSAssumeRoleProvider) GetCredential(objectReference string) (*proto.Credential, error) {
// sessionName is just a label, there can be many sessions with the same name
sessionName := "spiffe-connector"
input := &sts.AssumeRoleInput{
DurationSeconds: &p.duration,
RoleSessionName: &sessionName,
RoleArn: aws.String(objectReference),
}
result, err := p.stsService.AssumeRole(input)
if err != nil {
if aerr, ok := err.(awserr.Error); ok {
return &proto.Credential{}, fmt.Errorf("failed to get temporary credentials from STS: %s: %s", aerr.Code(), aerr.Message())
}
return &proto.Credential{}, fmt.Errorf("failed to get temporary credentials from STS: %w", err)
}
credentialsFile := fmt.Sprintf(`[default]
aws_access_key_id = %s
aws_secret_access_key = %s
aws_session_token = %s
`,
*result.Credentials.AccessKeyId,
*result.Credentials.SecretAccessKey,
*result.Credentials.SessionToken,
)
return &proto.Credential{
NotAfter: timestamppb.New(*result.Credentials.Expiration),
Files: []*proto.File{
{
Path: "~/.aws/credentials",
Mode: 0644,
Contents: []byte(credentialsFile),
},
},
}, nil
}