Skip to content
This repository was archived by the owner on Jan 22, 2025. It is now read-only.

New Feature: Two Man Realms #68

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 3 additions & 31 deletions docker/Dockerfile-ca
Original file line number Diff line number Diff line change
@@ -1,38 +1,10 @@
# This dockerfile builds a container capable of running the SSH CA bot. Note that a lot of this code is duplicated
# between this file and Dockerfile-kssh.
FROM ubuntu:18.04

# Dependencies
RUN apt-get -qq update
RUN apt-get -qq install curl software-properties-common ca-certificates gnupg -y
RUN useradd -ms /bin/bash keybase
USER keybase
WORKDIR /home/keybase

# Download and verify the deb
# Key fingerprint from https://keybase.io/docs/server_security/our_code_signing_key
RUN curl --remote-name https://prerelease.keybase.io/keybase_amd64.deb
RUN curl --remote-name https://prerelease.keybase.io/keybase_amd64.deb.sig
# Import our gpg key from our website. Pulling from key servers caused a flakey build so
# we get the key from the Keybase website instead.
RUN curl -sSL https://keybase.io/docs/server_security/code_signing_key.asc | gpg --import
# This line will error if the fingerprint of the key in the file does not match the
# known fingerprint of the our PGP key
RUN gpg --fingerprint 222B85B0F90BE2D24CFEB93F47484E50656D16C7
# And then verify the signature now that we have the key
RUN gpg --verify keybase_amd64.deb.sig keybase_amd64.deb

# Silence the error from dpkg about failing to configure keybase since `apt-get install -f` fixes it
USER root
RUN dpkg -i keybase_amd64.deb || true
RUN apt-get install -fy
USER keybase
# See docker/Dockerfile-keybase
FROM keybase:latest

# Install go
USER root
RUN add-apt-repository ppa:gophers/archive -y
RUN apt-get update
RUN apt-get install golang-1.11-go git sudo -y
RUN apt-get update && apt-get install golang-1.11-go git sudo -y
USER keybase

# Install go dependencies (speeds up future builds)
Expand Down
26 changes: 26 additions & 0 deletions docker/Dockerfile-keybase
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# This dockerfile builds an Ubuntu container with Keybase installed
FROM ubuntu:18.04

# Dependencies
RUN apt-get -qq update && apt-get -qq install curl software-properties-common ca-certificates gnupg -y
RUN useradd -ms /bin/bash keybase
USER keybase
WORKDIR /home/keybase

# Download and verify the deb
RUN curl --remote-name https://prerelease.keybase.io/keybase_amd64.deb
RUN curl --remote-name https://prerelease.keybase.io/keybase_amd64.deb.sig
# Import our gpg key from our website. Pulling from key servers caused a flakey build so
# we get the key from the Keybase website instead.
RUN curl -sSL https://keybase.io/docs/server_security/code_signing_key.asc | gpg --import
# This line will error if the fingerprint of the key in the file does not match the
# known fingerprint of the our PGP key
RUN gpg --fingerprint 222B85B0F90BE2D24CFEB93F47484E50656D16C7
# And then verify the signature now that we have the key
RUN gpg --verify keybase_amd64.deb.sig keybase_amd64.deb

# Silence the error from dpkg about failing to configure keybase since `apt-get install -f` fixes it
USER root
RUN dpkg -i keybase_amd64.deb || true
RUN apt-get install -fy
USER keybase
1 change: 1 addition & 0 deletions docker/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ SHELL := /bin/bash

# Build a new docker image for the CA bot
build: reset-permissions
docker build -t keybase -f Dockerfile-keybase .
docker build -t ca -f Dockerfile-ca ..

# Generate a new CA key
Expand Down
39 changes: 39 additions & 0 deletions docs/env.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,45 @@ export ANNOUNCEMENT="Hello! I'm {USERNAME} and I'm an SSH bot! I'm currently lis
export ANNOUNCEMENT="Hello! I'm {USERNAME} and I'm an SSH bot! Being in {CURRENT_TEAM} will grant you SSH access to certain servers. Reach out to @dworken for more information."
```

## Two Man Realms

It is possible to configure the SSH CA bot to require approval prior to granting access. For example, one could require
that in order to use kssh with servers in the `team.ssh.root_everywhere` realm two other people must approve the request.
Approvals are done by reacting with a :+1: emoji to the message inside of Keybase chat.

In order to configure this, there are 3 environment variables that can be used:

```
export TWO_MAN_TEAMS="team.ssh.root_everywhere, team.ssh.ci"
export TWO_MAN_APPROVERS="username0, username1, username2"
export TWO_MAN_APPROVER_COUNT="2"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TWO_MAN_ is a little confusing since the number of approvers is variable. it sounds to me like this is an M of N control policy. can we call it something like

CTRL_POLICY_1_MOFN_TEAMS="team.ssh.root_everywhere, team.ssh.ci"
CTRL_POLICY_1_M=2
CTRL_POLICY_1_N="username0, username1, username2"

is it possible we could want multiple control policies managed by a single bot? if so, this might need to be more complex. that's why i shoved a 1 in there. maybe not necessary right now. i dunno.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done! I think for now it isn't necessary to have the 1 in there since that seems like a pretty advanced feature and probably beyond the scope of this project.

```

The above will require that anyone who wants to use kssh with `team.ssh.root_everywhere` or `team.ssh.ci` gets approval
from two other people. Only three people are configured to approve requests: `username0, username1, username2`.

A few other example configurations are:

```
export TWO_MAN_TEAMS="team.ssh.root_everywhere"
export TWO_MAN_APPROVERS="username"
```

This would require that in order to use kssh with `team.ssh.root_everywhere` it must be approved by `username`.

As a user, this would be done by running:

```
kssh --request-realm team.ssh.root_everywhere root@production
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is kind of a mouthful. any chance it can work more like:

> kssh root@production
ERROR: awaiting approval in team.ssh.root_everywhere
...
> kssh root@production

OR

> kssh root@production
ERROR: you need to request approval first like: `kssh --request root@production`
> kssh --request root@production
...
> kssh root@production

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sadly, there isn't really a way to make that work :/

In your first example, there is no way for kssh to know which servers need which principals. This becomes especially hard when talking about ssh configs which can define custom names like "production" for a hostname.

The second example runs into essentially the same issue. While kssh could attempt to go first and then alert the user if the connection fails, there is no way for kssh --request root@production to know which principals to request.

A feature that could be built out is allowing the user to configure this sort of thing on the client side. Eg:

kssh root@production   # permission denied
kssh --configure-realm team.ssh.root_everywhere root@production  # configures kssh to always request that principal when accessing the given host
kssh root@production  # works 

The difficulty with the local config strategy is that it will break in weird ways when using proxy commands or any more advanced SSH features. Eg kssh -J root@production david@other. kssh wouldn't see the root@production connection and wouldn't know to provision a certificate with that principal. So IMO it doesn't seem worth it to build this feature given how brittle it would end up being, but certainly open to it if you think it would still be useful.

Copy link
Contributor

@xgess xgess Nov 1, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it seems really sub-optimal to me to push the onus of knowing how everything is set-up down to the kssh user.

In your first example, there is no way for kssh to know which servers need which principals

but doesn't the bot know? or if not, can we make it know with additional environment variables?
it looks like the message requesting approval is coming from the kssh user. can it come from the bot instead? this will make it much easier for an operations team to iterate on how they want this thing to work for them without having to push an update to all kssh users, which i imagine would be much more difficult.

```

For a user, the standard commands would work for all other configured teams:

```
kssh dev@staging
kssh dev@prod
```

## Developer Options

These environment variables are mainly useful for dev work. For security reasons, it is recommended always to run a
Expand Down
3 changes: 2 additions & 1 deletion integrationTest.sh
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ cd tests/
reset_docker

echo "Building containers..."
cd ../docker/ && make && cd ../tests/
cd ../docker/ && make build && cd ../tests/
pwd
docker-compose build
echo "Running integration tests..."
docker-compose up -d
Expand Down
71 changes: 39 additions & 32 deletions src/cmd/kssh/kssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,27 +20,27 @@ import (

func main() {
kssh.InitLogging()
team, remainingArgs, action, err := handleArgs(os.Args[1:])
botname, requestedPrincipal, remainingArgs, action, err := handleArgs(os.Args[1:])
if err != nil {
fmt.Printf("Failed to parse arguments: %v\n", err)
os.Exit(1)
}
keyPath, err := getSignedKeyLocation(team)
keyPath, err := getSignedKeyLocation(botname)
if err != nil {
fmt.Printf("Failed to retrieve location to store SSH keys: %v\n", err)
os.Exit(1)
}
if isValidCert(keyPath) {
if isValidCert(keyPath) && requestedPrincipal == "" {
log.WithField("keyPath", keyPath).Debug("Reusing unexpired certificate")
doAction(action, keyPath, remainingArgs)
os.Exit(0)
}
config, err := getConfig(team)
config, err := getConfig(botname)
if err != nil {
fmt.Printf("%v\n", err)
os.Exit(1)
}
err = provisionNewKey(config, keyPath)
err = provisionNewKey(config, keyPath, requestedPrincipal)
if err != nil {
fmt.Printf("%v\n", err)
os.Exit(1)
Expand Down Expand Up @@ -104,6 +104,7 @@ var cliArguments = []kssh.CLIArgument{
{Name: "--help", HasArgument: false},
{Name: "-v", HasArgument: false, Preserve: true},
{Name: "--set-keybase-binary", HasArgument: true},
{Name: "--request-realm", HasArgument: true},
}

var VersionNumber = "master"
Expand Down Expand Up @@ -131,7 +132,8 @@ GLOBAL OPTIONS:
--set-default-user Set the default SSH user to be used for kssh. Useful if you use ssh configs that do not set
a default SSH user
--clear-default-user Clear the default SSH user
--set-keybase-binary Run kssh with a specific keybase binary rather than resolving via $PATH `, VersionNumber)
--set-keybase-binary Advanced feature: Run kssh with a specific keybase binary rather than resolving via $PATH
--request-realm Advanced feature: Request a specific two-man realm in your provisioned certificate `, VersionNumber)
}

type Action int
Expand All @@ -141,55 +143,56 @@ const (
SSH
)

// Returns botname, remaining arguments, action, error
// Returns botname, requestedPrincipal, remaining arguments, action, error
// If the argument requires exiting after processing, it will call os.Exit
func handleArgs(args []string) (string, []string, Action, error) {
func handleArgs(args []string) (string, string, []string, Action, error) {
remaining, found, err := kssh.ParseArgs(args, cliArguments)
if err != nil {
return "", nil, 0, fmt.Errorf("Failed to parse provided arguments: %v", err)
return "", "", nil, 0, fmt.Errorf("Failed to parse provided arguments: %v", err)
}

team := ""
requestedPrincipal := ""
botname := ""
action := SSH
for _, arg := range found {
if arg.Argument.Name == "--bot" {
team = arg.Value
botname = arg.Value
}
if arg.Argument.Name == "--set-default-user" {
err := kssh.SetDefaultSSHUser(arg.Value)
if arg.Argument.Name == "--set-default-bot" {
// We exit immediately after setting the default bot
err := kssh.SetDefaultBot(arg.Value)
if err != nil {
fmt.Printf("Failed to set the default ssh user: %v\n", err)
fmt.Printf("Failed to set the default bot: %v\n", err)
os.Exit(1)
}
fmt.Println("Set default ssh user, exiting...")
fmt.Println("Set default bot, exiting...")
os.Exit(0)
}
if arg.Argument.Name == "--clear-default-user" {
err := kssh.SetDefaultSSHUser("")
if arg.Argument.Name == "--clear-default-bot" {
err := kssh.SetDefaultBot("")
if err != nil {
fmt.Printf("Failed to clear the default ssh user: %v\n", err)
fmt.Printf("Failed to clear the default bot: %v\n", err)
os.Exit(1)
}
fmt.Println("Cleared default ssh user, exiting...")
fmt.Println("Cleared default bot, exiting...")
os.Exit(0)
}
if arg.Argument.Name == "--set-default-bot" {
// We exit immediately after setting the default bot
err := kssh.SetDefaultBot(arg.Value)
if arg.Argument.Name == "--set-default-user" {
err := kssh.SetDefaultSSHUser(arg.Value)
if err != nil {
fmt.Printf("Failed to set the default bot: %v\n", err)
fmt.Printf("Failed to set the default ssh user: %v\n", err)
os.Exit(1)
}
fmt.Println("Set default bot, exiting...")
fmt.Println("Set default ssh user, exiting...")
os.Exit(0)
}
if arg.Argument.Name == "--clear-default-bot" {
err := kssh.SetDefaultBot("")
if arg.Argument.Name == "--clear-default-user" {
err := kssh.SetDefaultSSHUser("")
if err != nil {
fmt.Printf("Failed to clear the default bot: %v\n", err)
fmt.Printf("Failed to clear the default ssh user: %v\n", err)
os.Exit(1)
}
fmt.Println("Cleared default bot, exiting...")
fmt.Println("Cleared default ssh user, exiting...")
os.Exit(0)
}
if arg.Argument.Name == "--set-keybase-binary" {
Expand All @@ -211,8 +214,11 @@ func handleArgs(args []string) (string, []string, Action, error) {
if arg.Argument.Name == "-v" {
log.SetLevel(log.DebugLevel)
}
if arg.Argument.Name == "--request-realm" {
requestedPrincipal = arg.Value
}
}
return team, remaining, action, nil
return botname, requestedPrincipal, remaining, action, nil
}

// Get the kssh.ConfigFile. botname is the team specified via --bot if one was specified, otherwise the empty string
Expand Down Expand Up @@ -289,7 +295,7 @@ func isValidCert(keyPath string) bool {
}

// Provision a new signed SSH key with the given config
func provisionNewKey(config kssh.ConfigFile, keyPath string) error {
func provisionNewKey(config kssh.ConfigFile, keyPath string, requestedPrincipal string) error {
log.Debug("Generating a new SSH key...")

// Make ~/.ssh/ in case it doesn't exist
Expand All @@ -316,8 +322,9 @@ func provisionNewKey(config kssh.ConfigFile, keyPath string) error {

log.Debug("Requesting signature from the CA....")
resp, err := kssh.GetSignedKey(config, shared.SignatureRequest{
UUID: randomUUID.String(),
SSHPublicKey: string(pubKey),
UUID: randomUUID.String(),
SSHPublicKey: string(pubKey),
RequestedPrincipal: requestedPrincipal,
})
if err != nil {
return fmt.Errorf("Failed to get a signed key from the CA: %v", err)
Expand Down
Loading