-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Rodrigo Chacon <[email protected]>
- Loading branch information
Showing
22 changed files
with
1,290 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
.terraform | ||
*tfstate* | ||
*tfvars |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
.terraform/ | ||
*.tfstate | ||
*.tfstate* | ||
*.tfvars | ||
bastrd | ||
bastrd.gz | ||
tmp/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
FROM golang:1.11-alpine as build | ||
ENV GO111MODULE=on | ||
RUN apk add --no-cache ca-certificates git | ||
COPY go.mod /go/src/github.com/rochacon/bastrd/go.mod | ||
COPY go.sum /go/src/github.com/rochacon/bastrd/go.sum | ||
WORKDIR /go/src/github.com/rochacon/bastrd | ||
RUN go mod download | ||
COPY . /go/src/github.com/rochacon/bastrd | ||
RUN go install -v -ldflags "-X main.VERSION=$(git describe --abbrev=10 --always --dirty --tags)" -tags "netgo osusergo" | ||
|
||
FROM alpine | ||
COPY --from=build /etc/ssl/certs /etc/ssl/certs | ||
COPY --from=build /go/bin/bastrd /bastrd | ||
ENTRYPOINT ["/bastrd"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
FROM alpine | ||
RUN adduser -D -s /bin/sh user | ||
RUN apk add --no-cache ca-certificates curl git groff httpie jq py3-pip openssh-client tmux vim \ | ||
&& pip3 install awscli | ||
USER user | ||
WORKDIR /home/user |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
CONTAINER_IMAGE ?= "rochacon/bastrd" | ||
CONTAINER_IMAGE_TOOLBOX ?= "rochacon/bastrd-toolbox" | ||
VERSION ?= $$(git describe --abbrev=10 --always --dirty --tags) | ||
|
||
default: binary | ||
|
||
all: test binary image toolbox publish_binary publish_image publish_toolbox | ||
|
||
binary: | ||
go build -v -ldflags "-X main.VERSION=$(VERSION)" -tags "netgo osusergo" | ||
|
||
image: | ||
docker build -t $(CONTAINER_IMAGE):$(VERSION) . | ||
|
||
publish: publish_binary publish_image publish_image_toolbox | ||
|
||
publish_binary: | ||
gzip -f bastrd | ||
aws s3 cp ./bastrd.gz s3://bastrd-dev/bastrd.gz --acl public-read | ||
|
||
publish_image: | ||
docker push $(CONTAINER_IMAGE):$(VERSION) | ||
|
||
publish_toolbox: | ||
docker push $(CONTAINER_IMAGE_TOOLBOX):$(VERSION) | ||
|
||
test: | ||
go test $(ARGS) ./... | ||
|
||
toolbox: | ||
docker build -f Dockerfile.toolbox -t $(CONTAINER_IMAGE_TOOLBOX):$(VERSION) . | ||
|
||
.PHONY: binary image publish publish_binary publish_image publish_toolbox test toolbox |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
# bastrd - bastion server for secure environments | ||
|
||
`bastrd` builds on top of the ideas behind [keymaker](https://github.com/kislyuk/keymaker/) and [toolbox](https://github.com/coreos/toolbox) to build a secure shared bastion server for restricted environments. | ||
|
||
:warning: `bastrd` is in early development stage | ||
|
||
## How does it work? | ||
|
||
`bastrd` has 3 components: | ||
|
||
1. `bastrd sync`, an agent to sync AWS IAM groups and users to Linux | ||
1. `bastrd authorized-keys`, SSH authorized keys command to authenticate the user login against AWS IAM registered SSH Public Keys and groups | ||
1. `bastrd toolbox`, a session wrapper for a customizable toolbox container, the user must provide an AWS IAM account MFA token for authentication and setup of the session scoped credentials. | ||
|
||
## Toolbox features | ||
|
||
The toolbox container has the following features: | ||
|
||
* Validates MFA against user's AWS IAM MFA device | ||
* Create temporary user session AWS credentials | ||
* Mount temporary credentials as `/home/user/.aws/` using a tmpfs mount | ||
* Customizable session container image for advanced tools, check `Dockerfile.toolbox` for the default settings | ||
* Session resuming, for easier recovery of connections issues | ||
* SSH-agent forwarding (note: doesn't work on session resuming) | ||
* Firewall rule to block containers from hijacking the AWS EC2 instance profile used by bastrd itself | ||
* Reduced container capabilities for improved security, e.g., no socket binding | ||
|
||
## Installing on AWS with Terraform | ||
|
||
This repository was configured to be used as a quick way to create a `bastrd` instance on your AWS environment, fork it and customize as necessary. | ||
|
||
1. Clone this repo | ||
1. Configure `main.tf` with your state and `terrraform.tfvars` for your desired settings and run `terraform init` | ||
1. Run `terraform apply` to bootstrap the CoreOS instance and setup required AWS IAM groups | ||
1. Now wait a few minutes while your instance starts and connect to it via `ssh -A my-iam-username@$(terraform output)` | ||
|
||
## Uninstall | ||
|
||
1. `terraform destroy` to remove instance and related resources |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
package cmd | ||
|
||
import ( | ||
"fmt" | ||
"log" | ||
"strings" | ||
|
||
"github.com/aws/aws-sdk-go/aws" | ||
"github.com/aws/aws-sdk-go/aws/session" | ||
"github.com/aws/aws-sdk-go/service/iam" | ||
"github.com/urfave/cli" | ||
) | ||
|
||
var AuthorizedKeys = cli.Command{ | ||
Name: "authorized-keys", | ||
Usage: "List AWS IAM user registered SSH public keys.", | ||
ArgsUsage: "username", | ||
Action: getAuthorizedKeysForUser, | ||
Aliases: []string{"authorized_keys"}, | ||
Flags: []cli.Flag{ | ||
cli.StringSliceFlag{ | ||
Name: "allowed-groups", | ||
Usage: "Comma separated list of AWS IAM Groups allowed to SSH. (defaults to bastrd)", | ||
}, | ||
}, | ||
} | ||
|
||
// getAuthorizedKeysForUser retrieves | ||
func getAuthorizedKeysForUser(ctx *cli.Context) error { | ||
username := ctx.Args().Get(0) | ||
if username == "" { | ||
return fmt.Errorf("Username argument is required.") | ||
} | ||
allowedGroups := ctx.StringSlice("allowed-groups") | ||
if len(allowedGroups) == 0 { | ||
allowedGroups = append(allowedGroups, "bastrd") | ||
} | ||
|
||
awsSession := session.Must(session.NewSession(&aws.Config{})) | ||
iamSvc := iam.New(awsSession) | ||
|
||
if !userBelongsToAllowedGroups(iamSvc, username, allowedGroups) { | ||
return fmt.Errorf("User %q is not allowed to SSH into this instance, this incident will be reported.", username) | ||
} | ||
|
||
keys, err := getUserSSHPublicKeys(iamSvc, username) | ||
if err != nil { | ||
return fmt.Errorf("Error while retrieving user SSH public keys for user %q: %s", username, err) | ||
} | ||
if len(keys) == 0 { | ||
return fmt.Errorf("Found no SSH public keys for user %q.", username) | ||
} | ||
fmt.Println(strings.Join(keys, "\n")) | ||
return nil | ||
} | ||
|
||
// getUserSSHPublicKeys retrieves AWS IAM user SSH public keys | ||
func getUserSSHPublicKeys(iamSvc awsIAM, username string) ([]string, error) { | ||
keys := []string{} | ||
sshKeys, err := iamSvc.ListSSHPublicKeys(&iam.ListSSHPublicKeysInput{ | ||
UserName: aws.String(username), | ||
}) | ||
if err != nil { | ||
return keys, err | ||
} | ||
for _, key := range sshKeys.SSHPublicKeys { | ||
if *key.Status != iam.StatusTypeActive { | ||
log.Printf("authorized-keys: skipping key %q, status %q", *key.SSHPublicKeyId, *key.Status) | ||
continue | ||
} | ||
k, err := iamSvc.GetSSHPublicKey(&iam.GetSSHPublicKeyInput{ | ||
Encoding: aws.String(iam.EncodingTypeSsh), | ||
SSHPublicKeyId: key.SSHPublicKeyId, | ||
UserName: aws.String(username), | ||
}) | ||
if err != nil { | ||
return keys, err | ||
} | ||
keys = append(keys, *k.SSHPublicKey.SSHPublicKeyBody) | ||
} | ||
return keys, nil | ||
} | ||
|
||
// userBelongsToAllowedGroups checks wether user is a member of SSH allowed groups | ||
func userBelongsToAllowedGroups(iamSvc awsIAM, username string, allowedGroups []string) bool { | ||
userGroups, err := iamSvc.ListGroupsForUser(&iam.ListGroupsForUserInput{ | ||
UserName: aws.String(username), | ||
}) | ||
if err != nil { | ||
log.Println("authorized-keys: iam.ListGroupsForUser returned error:", err) | ||
return false | ||
} | ||
for _, group := range userGroups.Groups { | ||
if stringIn(*group.GroupName, allowedGroups) { | ||
// log.Printf("authorized-keys: user %q belongs to allowed group %q", username, *group.GroupName) | ||
return true | ||
} | ||
} | ||
return false | ||
} | ||
|
||
// stringIn matches if a string exist in a string slice | ||
func stringIn(s string, ss []string) bool { | ||
for _, item := range ss { | ||
if s == item { | ||
return true | ||
} | ||
} | ||
return false | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
package cmd | ||
|
||
import ( | ||
"github.com/aws/aws-sdk-go/service/iam" | ||
"github.com/aws/aws-sdk-go/service/sts" | ||
) | ||
|
||
// awsIAM interface holds required method signatures of IAM for easier test mocking | ||
type awsIAM interface { | ||
CreateAccessKey(input *iam.CreateAccessKeyInput) (*iam.CreateAccessKeyOutput, error) | ||
DeleteAccessKey(input *iam.DeleteAccessKeyInput) (*iam.DeleteAccessKeyOutput, error) | ||
GetSSHPublicKey(input *iam.GetSSHPublicKeyInput) (*iam.GetSSHPublicKeyOutput, error) | ||
ListGroupsForUser(input *iam.ListGroupsForUserInput) (*iam.ListGroupsForUserOutput, error) | ||
ListSSHPublicKeys(input *iam.ListSSHPublicKeysInput) (*iam.ListSSHPublicKeysOutput, error) | ||
} | ||
|
||
// awsSTS interface holds required method signatures of STS for easier test mocking | ||
type awsSTS interface { | ||
AssumeRole(input *sts.AssumeRoleInput) (*sts.AssumeRoleOutput, error) | ||
GetCallerIdentity(input *sts.GetCallerIdentityInput) (*sts.GetCallerIdentityOutput, error) | ||
GetSessionToken(input *sts.GetSessionTokenInput) (*sts.GetSessionTokenOutput, error) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
package cmd | ||
|
||
import ( | ||
"fmt" | ||
"log" | ||
"os" | ||
"os/signal" | ||
"strings" | ||
"time" | ||
|
||
"github.com/rochacon/bastrd/pkg/user" | ||
|
||
"github.com/aws/aws-sdk-go/aws" | ||
"github.com/aws/aws-sdk-go/aws/session" | ||
"github.com/aws/aws-sdk-go/service/iam" | ||
"github.com/urfave/cli" | ||
) | ||
|
||
var Sync = cli.Command{ | ||
Name: "sync", | ||
Usage: "Sync AWS IAM users.", | ||
Action: syncMain, | ||
Aliases: []string{"sync-users", "sync_users"}, | ||
Flags: []cli.Flag{ | ||
cli.BoolFlag{ | ||
Name: "disable-sandbox", | ||
Usage: "Disable users sandboxed sessions.", | ||
}, | ||
cli.StringSliceFlag{ | ||
Name: "groups", | ||
Usage: "AWS IAM group names to be synced. ATTENTION: Make sure these groups names don't conflict with existent system groups.", | ||
}, | ||
cli.DurationFlag{ | ||
Name: "interval", | ||
Usage: "Time interval between sync loops.", | ||
}, | ||
}, | ||
} | ||
|
||
func syncMain(ctx *cli.Context) error { | ||
groups := ctx.StringSlice("groups") | ||
if len(groups) == 0 { | ||
return fmt.Errorf("You must provide at least 1 AWS IAM group name.") | ||
} | ||
interval := ctx.Duration("interval") | ||
if interval.Minutes() == 0.0 && interval.Seconds() == 0.0 { | ||
log.Println("Defaulting interval to 1m") | ||
interval = time.Second * 60 | ||
} | ||
quit := make(chan os.Signal) | ||
signal.Notify(quit, os.Interrupt) | ||
log.Println("Executing initial sync") | ||
err := syncGroupsUsers(ctx) | ||
if err != nil { | ||
log.Printf("initial sync failed: %s", err) | ||
} | ||
log.Printf("Initiating sync loop for groups: %s", strings.Join(groups, ", ")) | ||
for { | ||
select { | ||
case <-time.After(interval): | ||
log.Printf("Starting sync") | ||
err = syncGroupsUsers(ctx) | ||
if err != nil { | ||
return err | ||
} | ||
log.Printf("Finished sync") | ||
case <-quit: | ||
log.Println("Received SIGINT, quitting.") | ||
return nil | ||
} | ||
} | ||
} | ||
|
||
// syncGroupsUsers synchronizes users from AWS IAM | ||
func syncGroupsUsers(ctx *cli.Context) error { | ||
isSandboxed := ctx.Bool("disable-sandbox") == false | ||
groupNames := ctx.StringSlice("groups") | ||
groups := []*user.Group{} | ||
for _, name := range groupNames { | ||
groups = append(groups, &user.Group{Name: name}) | ||
} | ||
|
||
awsSession := session.Must(session.NewSession(&aws.Config{})) | ||
iamSvc := iam.New(awsSession) | ||
|
||
iamUsers, err := user.FromIAMGroups(iamSvc, groups...) | ||
if err != nil { | ||
return fmt.Errorf("failed to retrieve AWS IAM users list: %s", err) | ||
} | ||
sysUsers, err := user.FromSystemGroups(groups...) | ||
if err != nil { | ||
return fmt.Errorf("failed to retrieve system users list: %s", err) | ||
} | ||
|
||
// Ensure groups in the system | ||
for _, group := range groups { | ||
log.Printf("Ensuring group %q", group.Name) | ||
err = group.Ensure() | ||
if err != nil { | ||
log.Printf("Failed to ensure group %q in the system: %s", group.Name, err) | ||
continue | ||
} | ||
} | ||
|
||
// create AWS IAM users that do not exist in the system | ||
for _, u := range iamUsers.Diff(sysUsers) { | ||
log.Printf("Ensuring user %q", u.Username) | ||
err = u.Ensure(isSandboxed) | ||
if err != nil { | ||
log.Printf("Failed to ensure user %q in the system: %s", u.Username, err) | ||
continue | ||
} | ||
for _, g := range u.Groups { | ||
log.Printf("Ensuring user %q in group %q", u.Username, g.Name) | ||
err = g.EnsureUser(u) | ||
if err != nil { | ||
log.Printf("Failed to ensure user %q in the system group %q: %s", u.Username, g.Name, err) | ||
continue | ||
} | ||
} | ||
} | ||
|
||
// remove system users that aren't on AWS IAM anymore | ||
for _, u := range sysUsers.Diff(iamUsers) { | ||
log.Printf("Removing user %q from the system", u.Username) | ||
err = u.Remove() | ||
if err != nil { | ||
log.Printf("Failed to remove user %q from the system: %s", u.Username, err) | ||
continue | ||
} | ||
} | ||
return err | ||
} |
Oops, something went wrong.