Skip to content

Commit

Permalink
Add docker container support for ECS Server
Browse files Browse the repository at this point in the history
- Build new `aws-sso-cli-ecs-server` image and publish on Docker Hub
- Makes it easy to run ecs server inside of a docker container as a
  local service
- Automatically configures the SSL key pair & bearer token for you

Fixes: #569
  • Loading branch information
synfinatic committed Jul 5, 2024
1 parent 42feabc commit 8bb8c36
Show file tree
Hide file tree
Showing 16 changed files with 733 additions and 116 deletions.
34 changes: 34 additions & 0 deletions .github/workflows/docker-hub.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Publish Docker image

on:
release:
types: [published]

jobs:
push_to_registry:
name: Push Docker image to Docker Hub
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v4

- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: synfinatic
password: ${{ secrets.DOCKER_HUB_SECRET }}

- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: synfinatic/aws-sso-cli-ecs-server

- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

* Add support for HTTP Auth/`AWS_CONTAINER_AUTHORIZATION_TOKEN` env variable #516
* Add support for HTTPS #518
* Add Docker container support #569

## [v1.16.1] - 2024-06-13

Expand Down
22 changes: 22 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Stage 1: Build stage
FROM golang:1.21-alpine AS builder
RUN apk --no-cache add git gcc musl-dev make
WORKDIR /app

# Copy the source code into the container
COPY . .

# Build the Go application
RUN make

# Stage 2: Final stage
FROM alpine:latest

WORKDIR /app
# Copy the built binary from the previous stage
COPY --from=builder /app/dist/aws-sso .

# Set the entrypoint for the container
EXPOSE 4144

ENTRYPOINT ["./aws-sso", "ecs", "run", "--docker"]
11 changes: 8 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
PROJECT_VERSION := 1.17.0
DOCKER_REPO := synfinatic
PROJECT_NAME := aws-sso
PROJECT_VERSION := 1.17.0
DOCKER_REPO := synfinatic
PROJECT_NAME := aws-sso
DOCKER_PROJECT_NAME := aws-sso-cli-ecs-server

DIST_DIR ?= dist/
GOOS ?= $(shell uname -s | tr "[:upper:]" "[:lower:]")
Expand Down Expand Up @@ -267,3 +268,7 @@ serve-docs: ## Run mkdocs server on localhost:8000
-v $$(pwd):/docs \
-p 8000:8000 \
synfinatic/mkdocs-material:latest

docker: ## Build docker image
docker build -t $(DOCKER_REPO)/$(DOCKER_PROJECT_NAME):$(PROJECT_VERSION) \
-t $(DOCKER_REPO)/$(DOCKER_PROJECT_NAME):latest .
59 changes: 20 additions & 39 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 {
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
}

type EcsLoadCmd struct {
// AWS Params
Expand Down Expand Up @@ -69,15 +71,7 @@ func (cc *EcsLoadCmd) Run(ctx *RunContext) error {
}

func (cc *EcsProfileCmd) Run(ctx *RunContext) error {
clientCert, err := ctx.Store.GetEcsSslCert()
if err != nil {
return err
}
bearerToken, err := ctx.Store.GetEcsBearerToken()
if err != nil {
return err
}
c := client.NewECSClient(ctx.Cli.Ecs.Profile.Port, bearerToken, clientCert)
c := newClient(ctx.Cli.Ecs.Profile.Port, ctx)

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

func (cc *EcsUnloadCmd) Run(ctx *RunContext) error {
clientCert, err := ctx.Store.GetEcsSslCert()
if err != nil {
return err
}
bearerToken, err := ctx.Store.GetEcsBearerToken()
if err != nil {
return err
}
c := client.NewECSClient(ctx.Cli.Ecs.Unload.Port, bearerToken, clientCert)
c := newClient(ctx.Cli.Ecs.Unload.Port, ctx)

return c.Delete(ctx.Cli.Ecs.Unload.Profile)
}

// Loads our AWS API creds into the ECS Server
func ecsLoadCmd(ctx *RunContext, awssso *sso.AWSSSO, accountId int64, role string) error {
c := newClient(ctx.Cli.Ecs.Load.Port, ctx)

creds := GetRoleCredentials(ctx, awssso, accountId, role)

cache := ctx.Settings.Cache.GetSSO() // ctx.Settings.Cache.Refresh(awssso, ssoConfig, ctx.Cli.SSO)
Expand All @@ -130,31 +118,12 @@ func ecsLoadCmd(ctx *RunContext, awssso *sso.AWSSSO, accountId int64, role strin
log.WithError(err).Warnf("Unable to update cache")
}

// do something
clientCert, err := ctx.Store.GetEcsSslCert()
if err != nil {
return err
}
bearerToken, err := ctx.Store.GetEcsBearerToken()
if err != nil {
return err
}
c := client.NewECSClient(ctx.Cli.Ecs.Load.Port, bearerToken, clientCert)

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

func (cc *EcsListCmd) Run(ctx *RunContext) error {
clientCert, err := ctx.Store.GetEcsSslCert()
if err != nil {
return err
}
bearerToken, err := ctx.Store.GetEcsBearerToken()
if err != nil {
return err
}
c := client.NewECSClient(ctx.Cli.Ecs.Profile.Port, bearerToken, clientCert)
c := newClient(ctx.Cli.Ecs.Profile.Port, ctx)

profiles, err := c.ListProfiles()
if err != nil {
Expand Down Expand Up @@ -187,3 +156,15 @@ func listProfiles(profiles []ecs.ListProfilesResponse) error {

return err
}

func newClient(port int, ctx *RunContext) *client.ECSClient {
certChain, err := ctx.Store.GetEcsSslCert()
if err != nil {
log.Fatalf("Unable to get ECS SSL cert: %s", err)
}
bearerToken, err := ctx.Store.GetEcsBearerToken()
if err != nil {
log.Fatalf("Unable to get ECS bearer token: %s", err)
}
return client.NewECSClient(port, bearerToken, certChain)
}
120 changes: 86 additions & 34 deletions cmd/aws-sso/ecs_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"strings"

// "github.com/davecgh/go-spew/spew"
"github.com/synfinatic/aws-sso-cli/internal/ecs"
"github.com/synfinatic/aws-sso-cli/internal/ecs/server"
)

Expand All @@ -34,9 +35,10 @@ const (
)

type EcsCmd struct {
Run EcsRunCmd `kong:"cmd,help='Run the ECS Server'"`
Run EcsRunCmd `kong:"cmd,help='Run the ECS Server locally'"`
BearerToken EcsBearerTokenCmd `kong:"cmd,help='Configure the ECS Server/AWS Client bearer token'"`
Cert EcsCertCmd `kong:"cmd,help='Configure the ECS Server SSL certificate'"`
Docker EcsDockerCmd `kong:"cmd,help='Start the ECS Server in a Docker container'"`
Cert EcsCertCmd `kong:"cmd,help='Configure the ECS Server SSL certificate/private key'"`
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'"`
Expand All @@ -45,35 +47,68 @@ type EcsCmd struct {

type EcsRunCmd struct {
Port int `kong:"help='TCP port to listen on',env='AWS_SSO_ECS_PORT',default=4144"`
// hidden flags are for internal use only when running in a docker container
Docker bool `kong:"hidden,help='Enable Docker support for ECS Server'"`
}

func (cc *EcsRunCmd) Run(ctx *RunContext) error {
l, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", ctx.Cli.Ecs.Run.Port))
if err != nil {
return err
// Start the ECS Server
ip := "127.0.0.1"
if ctx.Cli.Ecs.Run.Docker {
ip = "0.0.0.0"
}

token, err := ctx.Store.GetEcsBearerToken()
l, err := net.Listen("tcp", fmt.Sprintf("%s:%d", ip, ctx.Cli.Ecs.Run.Port))
if err != nil {
return err
}
if token == "" {
log.Warnf("No authentication token set, use 'aws-sso ecs bearer-token' to set one")
}
var privateKey, certChain string
if privateKey, err = ctx.Store.GetEcsSslKey(); err != nil {
return err
} else if privateKey != "" {
// only get the certificate if the private key is set
certChain, err = ctx.Store.GetEcsSslCert()

var bearerToken, privateKey, certChain string
if ctx.Cli.Ecs.Run.Docker {
// fetch the creds from our temporary file mounted in the docker container
f, err := ecs.OpenSecurityFile(ecs.READ_ONLY)
if err != nil {
log.Warnf("Failed to open ECS credentials file: %s", err.Error())
} else {
creds, err := ecs.ReadSecurityConfig(f)
if err != nil {
return err
}
// have to manually close since defer won't work in this case
f.Close()
os.Remove(f.Name())

bearerToken = creds.BearerToken
privateKey = creds.PrivateKey
certChain = creds.CertChain
}
} else {
if bearerToken, err = ctx.Store.GetEcsBearerToken(); err != nil {
return err
}
log.Infof("Running ECS Server with SSL/TLS enabled")

if privateKey, err = ctx.Store.GetEcsSslKey(); err != nil {
return err
} else if privateKey != "" {
// only get the certificate if the private key is set
if certChain, err = ctx.Store.GetEcsSslCert(); err != nil {
return err
}
}
}

if bearerToken == "" {
log.Warnf("HTTP Auth: disabled. Use 'aws-sso ecs bearer-token' to enable")
} else {
log.Info("HTTP Auth: enabled")
}

if privateKey != "" && certChain != "" {
log.Infof("SSL/TLS: enabled")
} else {
log.Infof("Running ECS Server without SSL/TLS")
log.Warnf("SSL/TLS: disabled. Use 'aws-sso ecs cert' to enable")
}
s, err := server.NewEcsServer(context.TODO(), token, l, privateKey, certChain)

s, err := server.NewEcsServer(context.TODO(), bearerToken, l, privateKey, certChain)
if err != nil {
return err
}
Expand Down Expand Up @@ -102,31 +137,48 @@ func (cc *EcsBearerTokenCmd) Run(ctx *RunContext) error {
}

type EcsCertCmd struct {
CertChain string `kong:"short=c,type='existingfile',help='Path to certificate chain PEM file',predictor='allFiles',xor='key'"`
PrivateKey string `kong:"short=p,type='existingfile',help='Path to private key file PEM file',predictor='allFiles',xor='cert'"`
Delete bool `kong:"short=d,help='Delete the current SSL certificate key pair',xor='key,cert'"`
Load EcsCertLoadCmd `kong:"cmd,help='Load a new SSL certificate/private key into the ECS Server'"`
Delete EcsCertDeleteCmd `kong:"cmd,help='Delete the current SSL certificate/private key'"`
Print EcsCertPrintCmd `kong:"cmd,help='Print the current SSL certificate'"`
}

func (cc *EcsCertCmd) Run(ctx *RunContext) error {
// If delete flag is set, delete the key pair
if ctx.Cli.Ecs.Cert.Delete {
return ctx.Store.DeleteEcsSslKeyPair()
}
type EcsCertLoadCmd struct {
CertChain string `kong:"short=c,type='existingfile',help='Path to certificate chain PEM file',predictor='allFiles',required"`
PrivateKey string `kong:"short=p,type='existingfile',help='Path to private key file PEM file',predictor='allFiles'"`
}

if ctx.Cli.Ecs.Cert.CertChain == "" && ctx.Cli.Ecs.Cert.PrivateKey != "" {
return fmt.Errorf("if --private-key is set, --cert-chain must also be set")
}
type EcsCertDeleteCmd struct{}

func (cc *EcsCertDeleteCmd) Run(ctx *RunContext) error {
return ctx.Store.DeleteEcsSslKeyPair()
}

type EcsCertPrintCmd struct{}

// Else, save the key pair
privateKey, err := os.ReadFile(ctx.Cli.Ecs.Cert.PrivateKey)
func (cc *EcsCertPrintCmd) Run(ctx *RunContext) error {
cert, err := ctx.Store.GetEcsSslCert()
if err != nil {
return fmt.Errorf("failed to read private key file: %w", err)
return err
}
fmt.Println(cert)
return nil
}

certChain, err := os.ReadFile(ctx.Cli.Ecs.Cert.CertChain)
func (cc *EcsCertLoadCmd) Run(ctx *RunContext) error {
var privateKey, certChain []byte
var err error

certChain, err = os.ReadFile(ctx.Cli.Ecs.Cert.Load.CertChain)
if err != nil {
return fmt.Errorf("failed to read certificate chain file: %w", err)
}

if ctx.Cli.Ecs.Cert.Load.PrivateKey != "" {
privateKey, err = os.ReadFile(ctx.Cli.Ecs.Cert.Load.PrivateKey)
if err != nil {
return fmt.Errorf("failed to read private key file: %w", err)
}
}

return ctx.Store.SaveEcsSslKeyPair(privateKey, certChain)
}
Loading

0 comments on commit 8bb8c36

Please sign in to comment.