Skip to content

Commit

Permalink
Improve bearer token support
Browse files Browse the repository at this point in the history
- Configure the bearer token in the SecureStore
- Document using ssh with aws-sso and ECS Server
- Bump to v1.17.0

Fixes: #915
  • Loading branch information
synfinatic committed Jun 27, 2024
1 parent 446119c commit 61145d5
Show file tree
Hide file tree
Showing 9 changed files with 203 additions and 32 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
PROJECT_VERSION := 1.16.1
PROJECT_VERSION := 1.17.0
DOCKER_REPO := synfinatic
PROJECT_NAME := aws-sso

Expand Down
21 changes: 13 additions & 8 deletions cmd/aws-sso/ecs_client_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ import (
"github.com/synfinatic/gotable"
)

type EcsListCmd struct{}
type EcsListCmd struct {
Auth string `kong:"help='Full HTTP Authorization token to use for ECS Server',env='AWS_CONTAINER_AUTHORIZATION_TOKEN'"`
}

type EcsLoadCmd struct {
// AWS Params
Expand All @@ -42,15 +44,18 @@ type EcsLoadCmd struct {
Profile string `kong:"short='p',help='Name of AWS Profile to assume',predictor='profile',xor='account,role'"`

// Other params
Port int `kong:"help='TCP port of aws-sso ECS Server',env='AWS_SSO_ECS_PORT',default=4144"` // SEE ECS_PORT in ecs_cmd.go
Slotted bool `kong:"short='s',help='Load credentials in a unique slot using the ProfileName as the key'"`
Auth string `kong:"help='Full HTTP Authorization token to use for ECS Server',env='AWS_CONTAINER_AUTHORIZATION_TOKEN'"`
Port int `kong:"help='TCP port of aws-sso ECS Server',env='AWS_SSO_ECS_PORT',default=4144"` // SEE ECS_PORT in ecs_cmd.go
Slotted bool `kong:"short='s',help='Load credentials in a unique slot using the ProfileName as the key'"`
}

type EcsProfileCmd struct {
Port int `kong:"help='TCP port of aws-sso ECS Server',env='AWS_SSO_ECS_PORT',default=4144"`
Auth string `kong:"help='Full HTTP Authorization token to use for ECS Server',env='AWS_CONTAINER_AUTHORIZATION_TOKEN'"`
Port int `kong:"help='TCP port of aws-sso ECS Server',env='AWS_SSO_ECS_PORT',default=4144"`
}

type EcsUnloadCmd struct {
Auth string `kong:"help='Full HTTP Authorization token to use for ECS Server',env='AWS_CONTAINER_AUTHORIZATION_TOKEN'"`
Port int `kong:"help='TCP port of aws-sso ECS Server',env='AWS_SSO_ECS_PORT',default=4144"`
Profile string `kong:"short='p',help='Name of AWS Profile to unload',predictor='profile'"`
}
Expand All @@ -69,7 +74,7 @@ func (cc *EcsLoadCmd) Run(ctx *RunContext) error {
}

func (cc *EcsProfileCmd) Run(ctx *RunContext) error {
c := client.NewECSClient(ctx.Cli.Ecs.Profile.Port, ctx.Cli.Ecs.SecurityToken)
c := client.NewECSClient(ctx.Cli.Ecs.Profile.Port, ctx.Cli.Ecs.Profile.Auth)

profile, err := c.GetProfile()
if err != nil {
Expand All @@ -87,7 +92,7 @@ func (cc *EcsProfileCmd) Run(ctx *RunContext) error {
}

func (cc *EcsUnloadCmd) Run(ctx *RunContext) error {
c := client.NewECSClient(ctx.Cli.Ecs.Unload.Port, ctx.Cli.Ecs.SecurityToken)
c := client.NewECSClient(ctx.Cli.Ecs.Unload.Port, ctx.Cli.Ecs.Unload.Auth)

return c.Delete(ctx.Cli.Ecs.Unload.Profile)
}
Expand Down Expand Up @@ -115,14 +120,14 @@ func ecsLoadCmd(ctx *RunContext, awssso *sso.AWSSSO, accountId int64, role strin
}

// do something
c := client.NewECSClient(ctx.Cli.Ecs.Load.Port, ctx.Cli.Ecs.SecurityToken)
c := client.NewECSClient(ctx.Cli.Ecs.Load.Port, ctx.Cli.Ecs.Load.Auth)

log.Debugf("%s", spew.Sdump(rFlat))
return c.SubmitCreds(creds, rFlat.Profile, ctx.Cli.Ecs.Load.Slotted)
}

func (cc *EcsListCmd) Run(ctx *RunContext) error {
c := client.NewECSClient(ctx.Cli.Ecs.Profile.Port, ctx.Cli.Ecs.SecurityToken)
c := client.NewECSClient(ctx.Cli.Ecs.Profile.Port, ctx.Cli.Ecs.List.Auth)

profiles, err := c.ListProfiles()
if err != nil {
Expand Down
43 changes: 36 additions & 7 deletions cmd/aws-sso/ecs_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"context"
"fmt"
"net"
"strings"

// "github.com/davecgh/go-spew/spew"
"github.com/synfinatic/aws-sso-cli/internal/ecs/server"
Expand All @@ -32,12 +33,12 @@ const (
)

type EcsCmd struct {
Run EcsRunCmd `kong:"cmd,help='Run the ECS Server'"`
List EcsListCmd `kong:"cmd,help='List profiles loaded in the ECS Server'"`
Load EcsLoadCmd `kong:"cmd,help='Load new IAM Role credentials into the ECS Server'"`
Unload EcsUnloadCmd `kong:"cmd,help='Unload the current IAM Role credentials from the ECS Server'"`
Profile EcsProfileCmd `kong:"cmd,help='Get the current role profile name in the default slot'"`
SecurityToken string `kong:"help='Security Token to use for authentication',env='AWS_CONTAINER_AUTHORIZATION_TOKEN'"`
Run EcsRunCmd `kong:"cmd,help='Run the ECS Server'"`
BearerToken EcsBearerTokenCmd `kong:"cmd,help='Configure the ECS Server bearer token'"`
List EcsListCmd `kong:"cmd,help='List profiles loaded in the ECS Server'"`
Load EcsLoadCmd `kong:"cmd,help='Load new IAM Role credentials into the ECS Server'"`
Unload EcsUnloadCmd `kong:"cmd,help='Unload the current IAM Role credentials from the ECS Server'"`
Profile EcsProfileCmd `kong:"cmd,help='Get the current role profile name in the default slot'"`
}

type EcsRunCmd struct {
Expand All @@ -49,9 +50,37 @@ func (cc *EcsRunCmd) Run(ctx *RunContext) error {
if err != nil {
return err
}
s, err := server.NewEcsServer(context.TODO(), ctx.Cli.Ecs.SecurityToken, l)

token, err := ctx.Store.GetEcsBearerToken()
if err != nil {
return err
}
if token == "" {
log.Warnf("No authentication token set, use 'aws-sso ecs bearer-token' to set one")
}

s, err := server.NewEcsServer(context.TODO(), token, l)
if err != nil {
return err
}
return s.Serve()
}

type EcsBearerTokenCmd struct {
Token string `kong:"short=t,help='Bearer token value to use for ECS Server',xor='flag'"`
Delete bool `kong:"short=d,help='Delete the current bearer token',xor='flag'"`
}

func (cc *EcsBearerTokenCmd) Run(ctx *RunContext) error {
// Store the token in the SecureStore
if ctx.Cli.Ecs.BearerToken.Delete {
return ctx.Store.DeleteEcsBearerToken()
}
if ctx.Cli.Ecs.BearerToken.Token == "" {
return fmt.Errorf("no token provided")
}
if !strings.HasPrefix(ctx.Cli.Ecs.BearerToken.Token, "Bearer ") {
return fmt.Errorf("token should start with 'Bearer '")
}
return ctx.Store.SaveEcsBearerToken(ctx.Cli.Ecs.BearerToken.Token)
}
49 changes: 49 additions & 0 deletions docs/remote-ssh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Using aws-sso on remote hosts with SSH

This is intended to show how to use your `aws-sso` credentials on a remote/bastion
host, without requring you to install or configure `aws-sso` on that host, while maintaining
security.

## Overview

**Note:** Before going any further, this document assumes you have already
[installed and configured](quickstart.md) aws-sso on your local system.
If not, do that now. :)

Accessing one or more AWS Identity Center based IAM Roles uses the [ECS Server](ecs-server.md)
feature and then running the ECS Server locally and using ssh to forward the port to the remote
host. Security is provided via a bearer token you configure on each side and all traffic is
encrypted over ssh.

**Note:** The root user or anyone with [CAP_NET_RAW or CAP_NET_ADMIN](https://man7.org/linux/man-pages/man7/capabilities.7.html)
will be able to intercept the HTTP traffic on the endpoint and obtain the bearer token
and/or any IAM Credentials stored in the ECS Server. As of this time, `aws-sso` does
not support HTTPS for full end-to-end encryption.

## On your local system

1. Configure a [bearer token](https://datatracker.ietf.org/doc/html/rfc6750#section-2.1)
for security to prevent unauthorized use of your IAM credentials:
`aws-sso ecs bearer-token -t 'Bearer <secret>`
1. Start the ECS Server (preferably in a screen or tmux session):
`aws-sso ecs run`
1. Load your selected IAM credentials into the ECS Server:
`aws-sso ecs load --profile=<aws profile name>`
1. SSH to the remote system using the [-R flag to forward tcp/4144](https://man.openbsd.org/ssh#R):
`ssh -R 4144:localhost:4144 <remotehost>`

## On your remote system (once you have logged in as described above)

1. Tell the AWS SDK how to talk to the ECS Server over SSH:
`export AWS_CONTAINER_CREDENTIALS_FULL_URI=http://localhost:4144/`
1. Tell the AWS SDK the bearer token secret from the first step on your local system:
`export AWS_CONTAINER_AUTHORIZATION_TOKEN='Bearer <secret>`
1. Verify everything works:
`aws sts get-caller-identity`

**Important:** You must choose a strong secret value for your bearer token secret! This is
what prevents anyone else from using your IAM credentials without your permission. Your bearer
token should be long and random enough to prevent bruteforce attacks.

See the [ECS Server documentation](ecs-server.md) for more information about the ECS server and
how to use multiple IAM role credentials simultaneously.
21 changes: 20 additions & 1 deletion internal/storage/json_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ type JsonStore struct {
CreateTokenResponse map[string]CreateTokenResponse `json:"CreateTokenResponse,omitempty"`
RoleCredentials map[string]RoleCredentials `json:"RoleCredentials,omitempty"` // ARN = key
StaticCredentials map[string]StaticCredentials `json:"StaticCredentials,omitempty"` // ARN = key
EcsBearerToken string `json:"EcsBearerToken,omitempty"`
}

// OpenJsonStore opens our insecure JSON storage backend
Expand All @@ -48,13 +49,14 @@ func OpenJsonStore(filename string) (*JsonStore, error) {
CreateTokenResponse: map[string]CreateTokenResponse{},
RoleCredentials: map[string]RoleCredentials{},
StaticCredentials: map[string]StaticCredentials{},
EcsBearerToken: "",
}

cacheBytes, err := os.ReadFile(filename)
if errors.Is(err, fs.ErrNotExist) {
return &cache, nil
} else if err != nil {
return &cache, fmt.Errorf("Unable to open %s: %s", filename, err.Error())
return &cache, fmt.Errorf("unable to open %s: %s", filename, err.Error())
}

if len(cacheBytes) > 0 {
Expand Down Expand Up @@ -184,3 +186,20 @@ func (jc *JsonStore) ListStaticCredentials() []string {
}
return ret
}

// SaveEcsBearerToken stores the token in the json file
func (jc *JsonStore) SaveEcsBearerToken(token string) error {
jc.EcsBearerToken = token
return jc.save()
}

// GetEcsBearerToken retrieves the token from the json file
func (jc *JsonStore) GetEcsBearerToken() (string, error) {
return jc.EcsBearerToken, nil
}

// DeleteEcsBearerToken deletes the token from the json file
func (jc *JsonStore) DeleteEcsBearerToken() error {
jc.EcsBearerToken = ""
return jc.save()
}
22 changes: 22 additions & 0 deletions internal/storage/json_store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,3 +206,25 @@ func (s *JsonStoreTestSuite) TestStaticCredentials() {
assert.NoError(t, s.json.GetStaticCredentials("arn:aws:iam::123456789012:user/foobar", &cr))
assert.Equal(t, cr2, cr)
}

func (s *JsonStoreTestSuite) TestEcsBearerToken() {
t := s.T()

token, err := s.json.GetEcsBearerToken()
assert.NoError(t, err)
assert.Empty(t, token)

err = s.json.SaveEcsBearerToken("not a real token")
assert.NoError(t, err)

token, err = s.json.GetEcsBearerToken()
assert.NoError(t, err)
assert.Equal(t, "not a real token", token)

err = s.json.DeleteEcsBearerToken()
assert.NoError(t, err)

token, err = s.json.GetEcsBearerToken()
assert.NoError(t, err)
assert.Empty(t, token)
}
Loading

0 comments on commit 61145d5

Please sign in to comment.