Skip to content

Commit

Permalink
Merge pull request #4 from keybase/david/integration-tests
Browse files Browse the repository at this point in the history
Add Dockerfile and integration tests
  • Loading branch information
ddworken authored Jul 16, 2019
2 parents a673945 + 34c1ca3 commit 0da8b47
Show file tree
Hide file tree
Showing 27 changed files with 474 additions and 25 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
bin/
keybaseca.config
nohup.out
env.sh
60 changes: 55 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
# SSHCA Bot
# SSH CA Bot

This repo contains a work in progress SSH CA bot built on top of Keybase. This project is not yet complete and is not
ready to be used.
The idea here is to control SSH access to servers (without needing to install anything on them) based simply on cryptographically provable membership in Keybase teams.

SSH supports a concept of certificate authorities (CAs) where you can place a single public key on the server, and the SSH server will allow any connections with keys signed by the CA cert. This is how a lot of large companies manage SSH access securely; users can be granted SSH access to servers without having to change the keys that are deployed on the server.

This repo provides the pieces for anyone to build this workflow:
1. generation scripts and a guide to set up the Keybase team and server ssh configuration
2. a wrapper around ssh (`kssh`) for any end user to get authenticated using the certificate authority
3. a chatbot (`keybaseca`) which listens in a Keybase team for `kssh` requests. If the requester is in the team, the bot will sign the request with an expiring signature (e.g. 1 hour), and then the provisioned server should authenticate as usual.

Removing a user's ability to access a server is as simple as removing them from the Keybase team.

This code is currently a work in progress and this project is not yet complete and is not ready to be used.

# Design

Expand All @@ -19,8 +29,48 @@ binaries.

`kssh` is the replacement SSH binary. It automatically pulls config files from KBFS.

# Getting Started (local environment)
# Integration Tests

This project contains integration tests that can be run via `./integrationTest.sh`. Note that prior to running
the integration tests you need to `cp tests/env.sh.example tests/env.sh` and fill in `tests/env.sh`.

# Getting Started (docker)

```bash
cd docker/
cp env.sh.example env.sh
keybase signup # Follow the prompts to create a new Keybase users to use for the SSH CA bot
keybase paperkey # Generate a new paper key
# Create a new Keybase subteam that this user is in along with anyone else you wish to grant SSH access to
nano env.sh # Fill in the values including the just generated paper key
make generate
```

This will output the public key for the CA.
For each server that you wish to make accessible to the CA bot:

1. Place the public key in `/etc/ssh/ca.pub`
2. Add the line `TrustedUserCAKeys /etc/ssh/ca.pub` to `/etc/ssh/sshd_config`
3. Restart ssh `service ssh restart`

Now start the chatbot itself:

```bash
make serve
```

Now build kssh and start SSHing!

```bash
go build -o bin/kssh cmd/kssh/kssh.go
sudo cp bin/kssh /usr/local/bin/ # Optional
bin/kssh root@server
```

Anyone else in `{TEAM}.ssh` can also run kssh in order to ssh into the server.

# Getting Started (local environment)
###### Recommended only for development work
In all of these directions, replace `{USER}` with your username and `{TEAM}` with the name of the team that you wish to
configure this bot for.

Expand Down Expand Up @@ -52,5 +102,5 @@ For each server that you wish to make accessible to the CA bot:

Now start the chatbot itself: `keybase --home /tmp/keybase service & go run cmd/keybaseca/keybaseca.go -c ~/keybaseca.config service` and leave it running.

Now you just run `go run cmd/kssh/kssh.go root@server` in order to SSH into your server. Anyone else in `{TEAM}.ssh` can
Now you run `go run cmd/kssh/kssh.go root@server` in order to SSH into your server. Anyone else in `{TEAM}.ssh` can
also run that command in order to ssh into the server.
17 changes: 14 additions & 3 deletions cmd/keybaseca/keybaseca.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"os"
"os/exec"
"path/filepath"
"strings"

"github.com/keybase/bot-ssh-ca/keybaseca/bot"
"github.com/keybase/bot-ssh-ca/keybaseca/config"
Expand Down Expand Up @@ -43,7 +44,7 @@ func main() {
if err != nil {
return err
}
err = sshutils.Generate(conf, c.Bool("overwrite-existing-key"), true)
err = sshutils.Generate(conf, c.Bool("overwrite-existing-key") || os.Getenv("FORCE_WRITE") == "true", true)
if err != nil {
return fmt.Errorf("Failed to generate a new key: %v", err)
}
Expand Down Expand Up @@ -86,7 +87,17 @@ func writeClientConfig(conf config.Config) error {

content, err := json.Marshal(kssh.ConfigFile{TeamName: conf.GetTeams()[0], BotName: username})

return ioutil.WriteFile(filename, content, 0600)
return KBFSWrite(filename, string(content))
}

func KBFSWrite(filename string, contents string) error {
cmd := exec.Command("keybase", "fs", "write", filename)
cmd.Stdin = strings.NewReader(string(contents))
bytes, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("Failed to write to file at %s: %s (%v)", filename, string(bytes), err)
}
return nil
}

func loadServerConfigAndWriteClientConfig(configFilename string) (config.Config, error) {
Expand Down
15 changes: 10 additions & 5 deletions cmd/kssh/kssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ func main() {
team, remainingArgs, err := handleArgs(os.Args)
if err != nil {
fmt.Printf("Failed to parse arguments: %v\n", err)
return
os.Exit(1)
}
keyPath, err := getSignedKeyLocation(team)
if isValidCert(keyPath) {
Expand All @@ -29,7 +29,7 @@ func main() {
config, err := getConfig(team)
if err != nil {
fmt.Printf("%v\n", err)
return
os.Exit(1)
}
provisionNewKey(config, keyPath)
runSSHWithKey(keyPath, remainingArgs)
Expand All @@ -51,9 +51,11 @@ func getSignedKeyLocation(team string) (string, error) {
}

// handleArgs parses os.Args for use with kssh. This is handwritten rather than using go's flag library (or
// any other CLI argument parsing library) since we want to have custom arguments and access any other arguments.
// any other CLI argument parsing library) since we want to have custom arguments and access any other remaining
// arguments. This function calls os.Exit(0) if it finds and handles a --set-default-team CLI flag.
// handleArgs returns (theDefaultTeam, theRemainingArguments, err)
func handleArgs(args []string) (string, []string, error) {
// TODO: Provide a way to clear default teams or at least a better message if there is a bad value there
if len(args) > 1 {
if args[1] == "--team" {
if len(args) == 2 {
Expand All @@ -65,11 +67,13 @@ func handleArgs(args []string) (string, []string, error) {
if len(args) == 2 {
return "", nil, fmt.Errorf("Got --set-default-team argument with no value!")
}
// We exit immediately after setting the default team
err := kssh.SetDefaultTeam(args[2])
if err != nil {
return "", nil, err
fmt.Printf("Failed to set the default team: %v", err)
os.Exit(1)
}
return "", args[3:], nil
os.Exit(0)
}
}
return "", args[1:], nil
Expand Down Expand Up @@ -189,6 +193,7 @@ func runSSHWithKey(keyPath string, remainingArgs []string) {
cmd.Stdin = os.Stdin
err := cmd.Run()
if err != nil {
fmt.Printf("SSH exited with err: %v", err)
os.Exit(1)
}
os.Exit(0)
Expand Down
33 changes: 33 additions & 0 deletions docker/Dockerfile-ca
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# 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

RUN apt-get -qq update
RUN apt-get -qq install curl software-properties-common -y
RUN useradd -ms /bin/bash keybase
USER keybase
WORKDIR /home/keybase
RUN curl --remote-name https://prerelease.keybase.io/keybase_amd64.deb
USER root

# Silence the error from dpkg about failing to configure keybase since `apt-get install -f` fixes it
RUN dpkg -i keybase_amd64.deb || true
RUN apt-get install -fy
USER keybase

# 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
USER keybase

# Install go dependencies (speeds up future builds)
COPY --chown=keybase go.mod .
COPY --chown=keybase go.sum .
RUN /usr/lib/go-1.11/bin/go mod download

COPY --chown=keybase ./ /home/keybase/
RUN /usr/lib/go-1.11/bin/go build -o bin/keybaseca cmd/keybaseca/keybaseca.go

USER root
18 changes: 18 additions & 0 deletions docker/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
SHELL := /bin/bash

.PHONY: generate serve generatekey

generate: | build generatekey

serve:
source env.sh && cat keybaseca.config.gen | envsubst > ../example-keybaseca-volume/keybaseca.config
source env.sh && docker run -e KEYBASE_USERNAME -e PAPERKEY -v $(PWD)/../example-keybaseca-volume:/mnt:rw ca:latest docker/entrypoint-server.sh

build:
docker build -t ca -f Dockerfile-ca ..

generatekey:
source env.sh && cat keybaseca.config.gen | envsubst > ../example-keybaseca-volume/keybaseca.config
source env.sh && docker run -e FORCE_WRITE -e KEYBASE_USERNAME -e PAPERKEY -v $(PWD)/../example-keybaseca-volume:/mnt:rw ca:latest docker/entrypoint-generate.sh
@echo -e '\nFor each server that you wish to make accessible to the CA bot:\n\n1. Place the public key in `/etc/ssh/ca.pub`\n2. Add the line `TrustedUserCAKeys /etc/ssh/ca.pub` to `/etc/ssh/sshd_config`\n3. Restart ssh `service ssh restart`'

15 changes: 15 additions & 0 deletions docker/entrypoint-generate.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/bin/bash
set -euo pipefail
IFS=$'\n\t'

# chown as root
chown keybase:keybase /mnt

# Run everything else as the keybase user
sudo -i -u keybase bash << EOF
export "FORCE_WRITE=$FORCE_WRITE"
nohup bash -c "run_keybase -g &"
sleep 3
keybase oneshot --username $KEYBASE_USERNAME --paperkey "$PAPERKEY"
bin/keybaseca -c /mnt/keybaseca.config generate
EOF
14 changes: 14 additions & 0 deletions docker/entrypoint-server.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/bin/bash
set -euo pipefail
IFS=$'\n\t'

# chown as root
chown keybase:keybase /mnt

# Run everything else as the keybase user
sudo -i -u keybase bash << EOF
nohup bash -c "run_keybase -g &"
sleep 3
keybase oneshot --username $KEYBASE_USERNAME --paperkey "$PAPERKEY"
bin/keybaseca -c /mnt/keybaseca.config service
EOF
8 changes: 8 additions & 0 deletions docker/env.sh.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/bin/bash
set -euo pipefail
IFS=$'\n\t'

# The subteam that will be used to grant SSH access
export SUBTEAM="teamname.subteam_for_ssh"
export KEYBASE_USERNAME="username_of_ca_bot"
export PAPERKEY="paper key for the ca bot"
8 changes: 8 additions & 0 deletions docker/keybaseca.config.gen
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Note that you do not need to edit this file. It is used with env.sh and envsubst in order to generate a config file
ca_key_location: /mnt/keybase-ca-key
key_expiration: "+1h"
ssh_user: root
teams:
- $SUBTEAM
keybase_paper_key: $PAPERKEY
keybase_username: $KEYBASE_USERNAME
3 changes: 3 additions & 0 deletions example-keybaseca-volume/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
*
!.gitignore
!README.md
1 change: 1 addition & 0 deletions example-keybaseca-volume/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This directory is used with the docker instructions as an example volume used to store the CA key. Do not override the gitignore in this file in order to commit any files in this directory.
35 changes: 35 additions & 0 deletions integrationTest.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#!/bin/bash
set -euo pipefail
IFS=$'\n\t'

# Some colors for pretty output
RED='\033[0;31m'
GREEN='\033[0;32m'
NC='\033[0m'

# A function used to indent the log output from the tests
indent() { sed 's/^/ /'; }

cd tests/
./reset.sh
source env.sh
cat keybaseca.config.gen | envsubst > keybaseca.config
echo "Building containers..."
docker-compose build 2>&1 > /dev/null
echo "Running integration tests..."
docker-compose up -d

TEST_EXIT_CODE=`docker wait tests_kssh_1`

docker logs tests_kssh_1 | indent

if [ -z ${TEST_EXIT_CODE+x} ] || [ "$TEST_EXIT_CODE" -ne 0 ] ; then
printf "${RED}Tests Failed${NC} - Exit Code: $TEST_EXIT_CODE\n"
else
printf "${GREEN}Tests Passed${NC}\n"
fi

docker-compose stop 2>&1 > /dev/null
docker-compose kill 2>&1 > /dev/null
docker-compose rm -f
./reset.sh
6 changes: 3 additions & 3 deletions keybaseca/sshutils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,14 @@ func GenerateNewSSHKey(filename string, overwrite bool, printPubKey bool) error
return err
}
} else {
return fmt.Errorf("Refusing to overwrite existing key (try with --overwrite-existing-key if you're sure): %s", filename)
return fmt.Errorf("Refusing to overwrite existing key (try with --overwrite-existing-key or FORCE_WRITE=true if you're sure): %s", filename)
}
}

cmd := exec.Command("ssh-keygen", "-t", "ed25519", "-f", filename, "-m", "PEM", "-N", "")
err := cmd.Run()
bytes, err := cmd.CombinedOutput()
if err != nil {
return err
return fmt.Errorf("ssh-keygen failed: %s (%v)", string(bytes), err)
}
if printPubKey {
bytes, err := ioutil.ReadFile(shared.KeyPathToPubKey(filename))
Expand Down
Loading

0 comments on commit 0da8b47

Please sign in to comment.