Skip to content

Commit 8215117

Browse files
author
Mike Morris
authored
Feature/long creds (#40)
* Allow for auto-refresh of role credentials when wrapping a command Instead of using a temporary credential file, start an HTTP server which mimics the ECS credential endpoint, and modify the environment such that the called command will reference the endpoint for credentials. This should allow the command to automatically refresh the role credentials while the process executes, for as long as the session token credentials are valid. * add `-E` flag to preserve env var credential behavior * Change MFA code prompt to use stderr, update aws sdk version * Set envvar to pass profile name to called program Expose the profile name, if it doesn't look like a role ARN, as the env var AWSRUNAS_PROFILE, so downstream programs can still get access to the profile name using a variable which won't collide with the AWS SDK operation. * generate/distribute only signed Windows executable * create .deb and .rpm packages for linux
1 parent 291f11f commit 8215117

File tree

15 files changed

+339
-31
lines changed

15 files changed

+339
-31
lines changed

.circleci/config.yml

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,26 @@
11
# Golang CircleCI 2.0 configuration file
22
#
33
# Check https://circleci.com/docs/2.0/language-go/ for more details
4-
version: 2
4+
version: 2.1
5+
6+
orbs:
7+
aws-cli: circleci/[email protected]
8+
59
jobs:
610
build:
711
docker:
812
# specify the version
913
- image: "circleci/golang:1.12"
1014

1115
working_directory: /go/src/github.com/mmmorris1975/aws-runas
16+
17+
environment:
18+
GO111MODULE: "on"
19+
1220
steps:
1321
- checkout
1422

1523
# specify any bash command here prefixed with `run: `
16-
# 'go get' only pulls from master branch, and circleci doesn't support dep
17-
- run: go get -v -t -d ./...
1824
- run: go vet -tests=false ./...
1925
- run: go test -v ./...
2026
- run: mkdir -p build
@@ -34,6 +40,11 @@ jobs:
3440
- attach_workspace:
3541
at: build
3642

43+
- aws-cli/install
44+
- aws-cli/configure:
45+
profile-name: circleci
46+
configure-default-region: false
47+
3748
- run: bundle check --path=vendor/bundle || bundle install --path=vendor/bundle --jobs=4 --retry=3
3849
- run: mkdir -p /var/tmp/rspec
3950
- run:

Makefile

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,24 +10,26 @@ $(EXE): go.mod *.go lib/*/*.go
1010
go build -v -ldflags '-X main.Version=$(VER)' -o $@
1111

1212
release: $(EXE) darwin windows linux
13+
docker run --rm -e VER=$(VER) -v ${PWD}:/build --entrypoint /build/scripts/package.sh debian:stretch
1314

1415
darwin linux:
1516
GOOS=$@ go build -ldflags '-X main.Version=$(VER)' -o $(EXE)-$(VER)-$@-$(GOARCH)
1617

1718
# $(shell go env GOEXE) is evaluated in the context of the Makefile host (before GOOS is evaluated), so hard-code .exe
1819
windows:
19-
GOOS=$@ go build -ldflags '-X main.Version=$(VER)' -o $(EXE)-$(VER)-$@-$(GOARCH).exe
20-
osslsigncode sign -certs .ca/codesign.crt -key .ca/codesign.key -n "aws-runas" -i https://github.com/mmmorris1975/aws-runas -in $(EXE)-$(VER)-$@-$(GOARCH).exe -out $(EXE)-$(VER)-$@-$(GOARCH)-signed.exe
20+
GOOS=$@ go build -ldflags '-X main.Version=$(VER)' -o $(EXE)-$(VER)-$@-$(GOARCH)-unsigned.exe
21+
osslsigncode sign -certs .ca/codesign.crt -key .ca/codesign.key -n "aws-runas" -i https://github.com/mmmorris1975/aws-runas -in $(EXE)-$(VER)-$@-$(GOARCH)-unsigned.exe -out $(EXE)-$(VER)-$@-$(GOARCH).exe
22+
rm -f $(EXE)-$(VER)-$@-$(GOARCH)-unsigned.exe
2123

2224
clean:
23-
rm -f $(EXE) $(EXE)-*-*-*
25+
rm -f $(EXE) $(EXE)-*-*-* $(EXE)*.rpm $(EXE)*.deb
2426

2527
dist-clean: clean
2628
rm -f go.sum
2729

2830
test: $(EXE)
2931
mv $(EXE) build
30-
go test -v ./...
32+
go test -count 1 -v ./...
3133
bundle install
3234
AWS_CONFIG_FILE=.aws/config AWS_PROFILE=arn:aws:iam::686784119290:role/circleci-role AWS_DEFAULT_PROFILE=circleci bundle exec rspec
3335

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ Pre-compiled binaries for various platforms can be downloaded [here](https://git
4141
-u, --update Check for updates to aws-runas
4242
-D, --diagnose Run diagnostics to gather info to troubleshoot issues
4343
--ec2 Run as mock EC2 metadata service to provide role credentials
44+
-E, --env Pass credentials to program as environment variables
4445
-V, --version Show application version.
4546

4647
Args:

docs/ec2-metadata.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,10 @@ http access log format.
5656

5757
## Program Access
5858
When executing programs which will get their credentials via this local metadata service, it may be necessary to set the
59-
`AWS_SHARED_CREDENTIALS_FILE` environment variable to an invalid value so the SDK does not attempt to use the credentials
60-
in that file to make the AWS service calls. This is due to the default AWS credential lookup chain checking the credentials
61-
file before attempting to get the credentials via the metadata service.
59+
`AWS_SHARED_CREDENTIALS_FILE` environment variable (`AWS_CREDENTIAL_PROFILES_FILE` if you're using the Java SDK) to an
60+
invalid value so the SDK does not attempt to use the credentials in that file to make the AWS service calls. This is due
61+
to the default AWS credential lookup chain checking the credentials file before attempting to get the credentials via the
62+
metadata service.
6263

6364
For example, running:
6465
```text

docs/execution.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ How to use aws-runas to perform various functions
77

88
#### Program Options
99
```text
10-
$ aws-runas --help
1110
usage: aws-runas [<flags>] [<profile>] [<cmd>...]
1211
1312
Create an environment for interacting with the AWS API using an assumed role
@@ -29,6 +28,7 @@ Flags:
2928
-u, --update Check for updates to aws-runas
3029
-D, --diagnose Run diagnostics to gather info to troubleshoot issues
3130
--ec2 Run as mock EC2 metadata service to provide role credentials
31+
-E, --env Pass credentials to program as environment variables
3232
-V, --version Show application version.
3333
3434
Args:
@@ -99,6 +99,15 @@ If necessary, the ARN for an MFA token can be provided via the `-M` command line
9999
$ aws-runas [-M mfa serial] arn:aws:iam::1234567890:role/my-role terraform plan
100100
```
101101

102+
#### Executing local docker containers using role credentials
103+
Special consideration must be given when executing docker containers which need to access AWS services using role credentials,
104+
and the container process does not handle the role assumption (it expects the role credentials to be provided to it). Use
105+
the `-E` command line option to instruct aws-runas to pass the credentials as environment variable to docker, like this:
106+
107+
```text
108+
$ aws-runas -E my-profile docker run -e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY -e AWS_SESSION_TOKEN -e AWS_REGION ...
109+
```
110+
102111
#### Injecting assume role credentials in the environment
103112
Running the program with only a profile name will output an eval()-able set of environment variables for the assumed role
104113
credentials which can be added to the current session.
@@ -142,6 +151,15 @@ credentials to minimize the possibility of the workflow getting disrupted by exp
142151
$ aws-runas -s admin-profile terraform plan
143152
```
144153

154+
#### Executing local docker containers using session token credentials
155+
Special consideration must be given when executing docker containers which need to access AWS services using session token
156+
credentials, typically for cases where the container app manages their own assume role activities. Use the `-E` command
157+
line option, along with the `-s` option to instruct aws-runas to pass the credentials as environment variable to docker, like this:
158+
159+
```text
160+
$ aws-runas -Es my-profile docker run -e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY -e AWS_SESSION_TOKEN -e AWS_REGION ...
161+
```
162+
145163
#### Injecting session token credentials in the environment
146164
Much like injecting role credentials into your session's environment, aws-runas supports injecting Session Token
147165
credentials in to your environment. It's simply a matter of executing aws-runas using the `-s` flag without specifying

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ require (
44
github.com/alecthomas/kingpin v2.2.6+incompatible
55
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc // indirect
66
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf // indirect
7-
github.com/aws/aws-sdk-go v1.18.6
7+
github.com/aws/aws-sdk-go v1.20.21
88
github.com/dustin/go-humanize v1.0.0
99
github.com/go-ini/ini v1.41.0
1010
github.com/mmmorris1975/aws-config v0.2.5

go.sum

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,12 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy
55
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY=
66
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
77
github.com/aws/aws-sdk-go v1.16.31/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
8-
github.com/aws/aws-sdk-go v1.17.4 h1:L2KFocQhg48kIzEAV98SnSz3nmIZ3UDFP+vU647KO3c=
9-
github.com/aws/aws-sdk-go v1.17.4/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
10-
github.com/aws/aws-sdk-go v1.18.6 h1:NuUz/+bi6C5v3BpIXW/VfovfMpvlhl1WUnD0EiDkOwQ=
11-
github.com/aws/aws-sdk-go v1.18.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
8+
github.com/aws/aws-sdk-go v1.20.21 h1:22vHWL9rur+SRTYPHAXlxJMFIA9OSYsYDIAHFDhQ7Z0=
9+
github.com/aws/aws-sdk-go v1.20.21/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
1210
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
1311
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
1412
github.com/go-ini/ini v1.41.0 h1:526aoxDtxRHFQKMZfcX2OG9oOI8TJ5yPLM0Mkno/uTY=
1513
github.com/go-ini/ini v1.41.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
16-
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
17-
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
1814
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
1915
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
2016
github.com/mmmorris1975/aws-config v0.2.5 h1:KTu/+lSQjepyPNRmf8l/jKzA04obUvAVuiWNPhVScmk=

lib/credentials/assume_role_provider.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"github.com/aws/aws-sdk-go/aws/credentials/stscreds"
99
"github.com/aws/aws-sdk-go/service/sts"
1010
"github.com/mmmorris1975/aws-runas/lib/cache"
11+
"os"
1112
"time"
1213
)
1314

@@ -168,12 +169,10 @@ func (p *AssumeRoleProvider) debug(f string, v ...interface{}) {
168169
}
169170
}
170171

171-
// StdinTokenProvider will print a prompt to Stdout for a user to enter the MFA code
172-
// fixme as of the 1.19.0 release of the aws go sdk, the prompt is printed on stderr instead of stdout
173-
// REF: https://github.com/aws/aws-sdk-go/pull/2481
172+
// StdinTokenProvider will print a prompt to Stderr for a user to enter the MFA code
174173
func StdinTokenProvider() (string, error) {
175174
var mfaCode string
176-
fmt.Print("Enter MFA Code: ")
175+
fmt.Fprint(os.Stderr, "Enter MFA Code: ")
177176
_, err := fmt.Scanln(&mfaCode)
178177
return mfaCode, err
179178
}

lib/credentials/assume_role_provider_test.go

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -246,12 +246,6 @@ func TestAssumeRoleProvider_WithLogger(t *testing.T) {
246246
}
247247
}
248248

249-
func Example_stdinTokenProvider() {
250-
StdinTokenProvider()
251-
// Output:
252-
// Enter MFA Code:
253-
}
254-
255249
func Example_roleDebugNilCfg() {
256250
p := new(AssumeRoleProvider)
257251
p.debug("test")
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package metadata
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"github.com/aws/aws-sdk-go/aws/credentials"
7+
simple_logger "github.com/mmmorris1975/simple-logger"
8+
"net"
9+
"net/http"
10+
"net/url"
11+
"time"
12+
)
13+
14+
const (
15+
// EcsCredentialsPath is the URL path used to retrieve the credentials
16+
EcsCredentialsPath = "/credentials"
17+
)
18+
19+
// EcsMetadataInput contains the options available for customizing the behavior of the ECS Metadata Service
20+
type EcsMetadataInput struct {
21+
// Credentials is the AWS credentials.Credentials object used to fetch the credentials. This allows us to have
22+
// the service return role credentials, or session credentials (in case the caller's code does its own role management)
23+
Credentials *credentials.Credentials
24+
// Logger is the logging object to configure for the service. If not provided, a standard logger is configured.
25+
Logger *simple_logger.Logger
26+
}
27+
28+
// EcsMetadataService is the object encapsulating the details of the service
29+
type EcsMetadataService struct {
30+
// Url is the fully-formed URL to use for retrieving credentials from the service
31+
Url *url.URL
32+
lsnr net.Listener
33+
}
34+
35+
// NewEcsMetadataService creates a new EcsMetadataService object using the provided EcsMetadataInput options.
36+
func NewEcsMetadataService(opts *EcsMetadataInput) (*EcsMetadataService, error) {
37+
cred = opts.Credentials
38+
log = opts.Logger
39+
if log == nil {
40+
log = simple_logger.StdLogger
41+
}
42+
43+
s := new(EcsMetadataService)
44+
45+
// The SDK seems to only support listening on "localhost" and 127.0.0.1, not the ::1 IPv6 loopback, try not to be clever
46+
l, err := net.Listen("tcp", "127.0.0.1:0")
47+
if err != nil {
48+
return nil, err
49+
}
50+
s.lsnr = l
51+
52+
u, err := url.Parse(fmt.Sprintf("http://%s%s", l.Addr(), EcsCredentialsPath))
53+
if err != nil {
54+
return nil, err
55+
}
56+
s.Url = u
57+
58+
return s, nil
59+
}
60+
61+
// Run starts the HTTP server used to fetch credentials. The HTTP server will listen on the loopback address on a
62+
// randomly chosen port.
63+
func (s *EcsMetadataService) Run() {
64+
http.HandleFunc(EcsCredentialsPath, ecsHandler)
65+
if err := http.Serve(s.lsnr, nil); err != nil {
66+
log.Error(err)
67+
}
68+
}
69+
70+
func ecsHandler(w http.ResponseWriter, r *http.Request) {
71+
var rc = http.StatusOK
72+
73+
v, err := cred.Get()
74+
if err != nil {
75+
rc = http.StatusInternalServerError
76+
j, err := json.Marshal(&ecsCredentialError{Code: string(rc), Message: err.Error()})
77+
if err != nil {
78+
log.Warnf("error converting error message to json: %v", err)
79+
}
80+
81+
w.WriteHeader(rc)
82+
w.Write(j)
83+
return
84+
}
85+
86+
e, err := cred.ExpiresAt()
87+
if err != nil {
88+
e = time.Now()
89+
}
90+
91+
c := ecsCredentials{
92+
AccessKeyId: v.AccessKeyID,
93+
SecretAccessKey: v.SecretAccessKey,
94+
Token: v.SessionToken,
95+
Expiration: e.UTC().Format(time.RFC3339),
96+
}
97+
log.Debugf("ECS endpoint credentials: %+v", c)
98+
99+
j, err := json.Marshal(&c)
100+
if err != nil {
101+
log.Warnf("error converting credentials to json: %v", err)
102+
}
103+
104+
w.WriteHeader(rc)
105+
w.Write(j)
106+
}
107+
108+
type ecsCredentialError struct {
109+
Code string `json:"code"`
110+
Message string `json:"message"`
111+
}
112+
113+
type ecsCredentials struct {
114+
AccessKeyId string
115+
SecretAccessKey string
116+
Token string
117+
Expiration string
118+
}

0 commit comments

Comments
 (0)