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

Commit 0fa0c3c

Browse files
committed
Merge branch 'master' into david/specify-key-in-ssh-config
2 parents 7455505 + 835c208 commit 0fa0c3c

14 files changed

Lines changed: 90 additions & 17 deletions

File tree

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
# SSH CA Bot
22

3+
[![License](https://img.shields.io/badge/license-BSD-success.svg)](https://opensource.org/licenses/BSD-3-Clause)
4+
[![CircleCI](https://circleci.com/gh/keybase/bot-sshca.svg?style=shield)](https://circleci.com/gh/keybase/bot-sshca)
5+
[![Go ReportCard](https://goreportcard.com/badge/github.com/keybase/bot-sshca)](https://goreportcard.com/report/github.com/keybase/bot-sshca)
6+
37
See [keybase.io/blog/keybase-ssh-ca](https://keybase.io/blog/keybase-ssh-ca) for a full announcement and description
48
of the code in this repository.
59

6-
The idea here is to control SSH access to servers (without needing to install anything on them) based simply on
7-
cryptographically provable membership in Keybase teams.
10+
This repository provides the tooling to control SSH access to servers (without needing to install anything
11+
on them) based simply on cryptographically provable membership in Keybase teams.
812

913
SSH supports a concept of certificate authorities (CAs) where you can place a single public key on the server,
1014
and the SSH server will allow any connections with keys signed by the CA cert. This is how a lot of large companies

src/cmd/keybaseca/keybaseca.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ func generateAction(c *cli.Context) error {
122122
}
123123
captureControlCToDeleteClientConfig(conf)
124124
defer deleteClientConfig(conf)
125-
err = sshutils.Generate(conf, c.Bool("overwrite-existing-key") || os.Getenv("FORCE_WRITE") == "true", true)
125+
err = sshutils.Generate(conf, c.Bool("overwrite-existing-key") || os.Getenv("FORCE_WRITE") == "true")
126126
if err != nil {
127127
return fmt.Errorf("Failed to generate a new key: %v", err)
128128
}

src/keybaseca/config/config.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,36 +116,45 @@ func validatePath(path string) error {
116116
return nil
117117
}
118118

119+
// A Config struct that pulls transparently from the environment
119120
type EnvConfig struct{}
120121

121122
var _ Config = (*EnvConfig)(nil)
122123

124+
// Get the location of the CA key
123125
func (ef *EnvConfig) GetCAKeyLocation() string {
124126
if os.Getenv("CA_KEY_LOCATION") != "" {
125127
return shared.ExpandPathWithTilde(os.Getenv("CA_KEY_LOCATION"))
126128
}
127129
return shared.ExpandPathWithTilde("/mnt/keybase-ca-key")
128130
}
129131

132+
// Get the keybase home directory. Used if you are running a separate instance of keybase for the chatbot. May be empty.
130133
func (ef *EnvConfig) GetKeybaseHomeDir() string {
131134
return os.Getenv("KEYBASE_HOME_DIR")
132135
}
133136

137+
// Get the keybase paper key for the bot account. Used if you are running a separate instance of keybase for the chatbot.
138+
// May be empty.
134139
func (ef *EnvConfig) GetKeybasePaperKey() string {
135140
return os.Getenv("KEYBASE_PAPERKEY")
136141
}
137142

143+
// Get the keybase username for the bot account. Used if you are running a separate instance of keybase for the chatbot.
144+
// May be empty.
138145
func (ef *EnvConfig) GetKeybaseUsername() string {
139146
return os.Getenv("KEYBASE_USERNAME")
140147
}
141148

149+
// Get the expiration period for signatures generated by the bot.
142150
func (ef *EnvConfig) GetKeyExpiration() string {
143151
if os.Getenv("KEY_EXPIRATION") != "" {
144152
return os.Getenv("KEY_EXPIRATION")
145153
}
146154
return "+1h"
147155
}
148156

157+
// Get the list of keybase teams configured to be used with the bot.
149158
func (ef *EnvConfig) GetTeams() []string {
150159
split := strings.Split(os.Getenv("TEAMS"), ",")
151160
var teams []string
@@ -158,6 +167,7 @@ func (ef *EnvConfig) GetTeams() []string {
158167
return teams
159168
}
160169

170+
// Get the location for the bot's audit logs. May be empty.
161171
func (ef *EnvConfig) GetLogLocation() string {
162172
return os.Getenv("LOG_LOCATION")
163173
}
@@ -166,14 +176,18 @@ func (ef *EnvConfig) getStrictLogging() string {
166176
return strings.ToLower(os.Getenv("STRICT_LOGGING"))
167177
}
168178

179+
// Get whether or not strict logging (see env.md for a description of this feature) is enabled
169180
func (ef *EnvConfig) GetStrictLogging() bool {
170181
return ef.getStrictLogging() == "true"
171182
}
172183

184+
// Get the Keybase chat location configured to be used for all communication. A chat channel consists of
185+
// team.subteam#channel-name. May be empty.
173186
func (ef *EnvConfig) getChatChannel() string {
174187
return os.Getenv("CHAT_CHANNEL")
175188
}
176189

190+
// Get the team used for all communication. May be empty.
177191
func (ef *EnvConfig) GetChatTeam() string {
178192
if ef.getChatChannel() == "" {
179193
return ""
@@ -185,6 +199,7 @@ func (ef *EnvConfig) GetChatTeam() string {
185199
return team
186200
}
187201

202+
// Get the channel used for all communication. May be empty.
188203
func (ef *EnvConfig) GetChannelName() string {
189204
if ef.getChatChannel() == "" {
190205
return ""

src/keybaseca/sshutils/generate_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/stretchr/testify/require"
1111
)
1212

13+
// Test generating a new SSH key
1314
func TestGenerateNewSSHKey(t *testing.T) {
1415
filename := "/tmp/bot-ssh-ca-integration-test-generate-key"
1516
os.Remove(filename)

src/keybaseca/sshutils/generate_unix.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@ import (
88
"strings"
99
)
1010

11-
// Generate a new SSH key. Places the private key at filename and the public key at filename.pub. If `overwrite`,
12-
// it will overwrite the existing key. If `printPubKey` it will print out the generated public key to stdout.
11+
// Generate a new SSH key. Places the private key at filename and the public key at filename.pub.
1312
// On unix, we use ed25519 keys since they may be more secure (and are smaller). The go crypto ssh library
1413
// does not support ed25519 keys so we use ssh-keygen in order to generate the key.
1514
func generateNewSSHKey(filename string) error {

src/keybaseca/sshutils/generate_windows.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,7 @@ import (
1515
"golang.org/x/crypto/ssh"
1616
)
1717

18-
// Generate a new SSH key. Places the private key at filename and the public key at filename.pub. If `overwrite`,
19-
// it will overwrite the existing key. If `printPubKey` it will print out the generated public key to stdout.
18+
// Generate a new SSH key. Places the private key at filename and the public key at filename.pub.
2019
// On windows, we use 2048 bit rsa keys. go's ssh library doesn't support ed25519 and ssh-keygen isn't built in.
2120
func generateNewSSHKey(filename string) error {
2221
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)

src/keybaseca/sshutils/sshutils.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import (
1717
"github.com/google/uuid"
1818
)
1919

20+
// Generate a new ssh key. Store the private key at filename and the public key at filename.pub. If overwrite, it will
21+
// overwrite anything at filename or filename.pub. If printPubKey, it will print the generated public key to stdout.
2022
func GenerateNewSSHKey(filename string, overwrite bool, printPubKey bool) error {
2123
_, err := os.Stat(filename)
2224
if err == nil {
@@ -46,8 +48,10 @@ func GenerateNewSSHKey(filename string, overwrite bool, printPubKey bool) error
4648
return nil
4749
}
4850

49-
func Generate(conf config.Config, overwrite bool, printPubKey bool) error {
50-
err := GenerateNewSSHKey(conf.GetCAKeyLocation(), overwrite, printPubKey)
51+
// Generate a new CA key based off of the data in the config. If overwrite, it will overwrite the current CA key. Prints
52+
// the generated public key to stdout.
53+
func Generate(conf config.Config, overwrite bool) error {
54+
err := GenerateNewSSHKey(conf.GetCAKeyLocation(), overwrite, true)
5155
if err == nil {
5256
log.Log(conf, fmt.Sprintf("Wrote new SSH CA key to %s", conf.GetCAKeyLocation()))
5357
}
@@ -69,6 +73,8 @@ func getTempFilename(pattern string) (string, error) {
6973
return tempFilename, nil
7074
}
7175

76+
// Process a given SignatureRequest into a SignatureResponse or an error. This consists of validating the signature request,
77+
// determining the correct principals, and signing the provided public key.
7278
func ProcessSignatureRequest(conf config.Config, sr shared.SignatureRequest) (resp shared.SignatureResponse, err error) {
7379
randomUUID, err := uuid.NewRandom()
7480
if err != nil {

src/kssh/bot.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ func GetSignedKey(config ConfigFile, request shared.SignatureRequest) (shared.Si
103103
}
104104
}
105105

106+
// Get the configured channel name from the given config file. Returns either a pointer to the channel name string
107+
// or a null pointer.
106108
func getChannel(config ConfigFile) *string {
107109
var channel *string
108110
if config.ChannelName != "" {

src/kssh/config.go

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,17 @@ import (
1111
"github.com/keybase/bot-ssh-ca/src/shared"
1212
)
1313

14-
// A ConfigFile that is provided by the keybaseca server process and lives in kbfs
14+
// A ConfigFile that is provided by the keybaseca server process and lives in kbfs. It is used to share configuration
15+
// information about how kssh should communicate with the keybaseca chatbot.
1516
type ConfigFile struct {
1617
TeamName string `json:"teamname"`
1718
ChannelName string `json:"channelname"`
1819
BotName string `json:"botname"`
1920
}
2021

2122
// LoadConfigs loads client configs from KBFS. Returns a (listOfConfigFiles, listOfBotNames, err)
22-
// Both lists are deduplicated based on ConfigFile.BotName
23+
// Both lists are deduplicated based on ConfigFile.BotName. Runs the KBFS operations in parallel
24+
// to speed up loading configs.
2325
func LoadConfigs() ([]ConfigFile, []string, error) {
2426
allTeamsFromKBFS, err := shared.KBFSList("/keybase/team/")
2527
if err != nil {
@@ -81,6 +83,7 @@ func LoadConfigs() ([]ConfigFile, []string, error) {
8183
return configs, botnames, nil
8284
}
8385

86+
// Load a kssh client config file from the given filename
8487
func LoadConfig(kbfsFilename string) (ConfigFile, error) {
8588
var cf ConfigFile
8689
if !strings.HasPrefix(kbfsFilename, "/keybase/") {
@@ -100,21 +103,26 @@ func LoadConfig(kbfsFilename string) (ConfigFile, error) {
100103
return cf, err
101104
}
102105

103-
// A LocalConfigFile is a file that lives on the FS of the computer running kssh. It is only used if the user is
104-
// in multiple teams that are running the CA bot. Note that we store the team in here (even though it wasn't specified
105-
// by the user) so that we can avoid doing a call to `LoadConfigs` if a default is set (since `LoadConfigs can be very
106-
// slow if the user is in a large number of teams).
106+
// A LocalConfigFile is a file that lives on the FS of the computer running kssh. By default (and for most users), this
107+
// file is not used.
107108
//
108-
// Controlled via `kssh --set-default-bot foo`.
109+
// If a user of kssh is in in multiple teams that are running the CA bot they can configure a default bot to communicate
110+
// with. Note that we store the team in here (even though it wasn't specified by the user) so that we can avoid doing
111+
// a call to `LoadConfigs` if a default is set (since `LoadConfigs can be very slow if the user is in a large number of teams).
112+
// This is controlled via `kssh --set-default-bot foo`.
113+
//
114+
// If a user of kssh wishes to configure a default ssh user to use (see README.md for a description of why this may
115+
// be useful) this is also stored in the local config file. This is controlled via `kssh --set-default-user foo`.
109116
type LocalConfigFile struct {
110117
DefaultBotName string `json:"default_bot"`
111118
DefaultBotTeam string `json:"default_team"`
112119
DefaultSSHUser string `json:"default_ssh_user"`
113120
}
114121

115-
// Where to store the local config file. Just stash it in ~/.ssh
122+
// Where to store the local config file. Just stash it in ~/.ssh rather than making a ~/.kssh folder
116123
var localConfigFileLocation = shared.ExpandPathWithTilde("~/.ssh/kssh-config.json")
117124

125+
// Get the default SSH user to use for kssh connections. Empty if no user is configured.
118126
func GetDefaultSSHUser() (string, error) {
119127
lcf, err := getCurrentConfig()
120128
if err != nil {
@@ -124,6 +132,7 @@ func GetDefaultSSHUser() (string, error) {
124132
return lcf.DefaultSSHUser, nil
125133
}
126134

135+
// Set the default SSH user to use for kssh connections.
127136
func SetDefaultSSHUser(username string) error {
128137
if strings.ContainsAny(username, " \t\n\r'\"") {
129138
return fmt.Errorf("invalid username: %s", username)
@@ -138,6 +147,7 @@ func SetDefaultSSHUser(username string) error {
138147
return writeConfig(lcf)
139148
}
140149

150+
// Write the given config file to disk
141151
func writeConfig(lcf LocalConfigFile) error {
142152
bytes, err := json.Marshal(&lcf)
143153
if err != nil {
@@ -157,6 +167,7 @@ func writeConfig(lcf LocalConfigFile) error {
157167
return nil
158168
}
159169

170+
// Get the current kssh config file
160171
func getCurrentConfig() (lcf LocalConfigFile, err error) {
161172
if _, err := os.Stat(localConfigFileLocation); os.IsNotExist(err) {
162173
return lcf, nil
@@ -172,10 +183,12 @@ func getCurrentConfig() (lcf LocalConfigFile, err error) {
172183
return lcf, nil
173184
}
174185

186+
// Set the default keybaseca bot to communicate with.
175187
func SetDefaultBot(botname string) error {
176188
teamname := ""
177189
var err error
178190
if botname != "" {
191+
// Get the team associated with it and cache that too in order to avoid looking it up everytime
179192
teamname, err = GetTeamFromBot(botname)
180193
if err != nil {
181194
return err
@@ -192,6 +205,7 @@ func SetDefaultBot(botname string) error {
192205
return writeConfig(lcf)
193206
}
194207

208+
// Get the default bot and team for kssh
195209
func GetDefaultBotAndTeam() (string, string, error) {
196210
lcf, err := getCurrentConfig()
197211
if err != nil {
@@ -200,6 +214,7 @@ func GetDefaultBotAndTeam() (string, string, error) {
200214
return lcf.DefaultBotName, lcf.DefaultBotTeam, nil
201215
}
202216

217+
// Get the teamname associated with the given botname
203218
func GetTeamFromBot(botname string) (string, error) {
204219
configs, _, err := LoadConfigs()
205220
if err != nil {

src/kssh/ssh.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/keybase/bot-ssh-ca/src/shared"
1010
)
1111

12+
// Add the SSH key at the given location to the currently running SSH agent. Errors if there is no running ssh-agent.
1213
func AddKeyToSSHAgent(keyPath string) error {
1314
cmd := exec.Command("ssh-add", keyPath)
1415
bytes, err := cmd.CombinedOutput()
@@ -64,6 +65,7 @@ func CreateDefaultUserConfigFile(keyPath string) error {
6465
return nil
6566
}
6667

68+
// Make the ~/.ssh/ folder if it does not exist
6769
func MakeDotSSH() error {
6870
if _, err := os.Stat(shared.ExpandPathWithTilde("~/.ssh/")); os.IsNotExist(err) {
6971
err = os.Mkdir(shared.ExpandPathWithTilde("~/.ssh/"), 0700)

0 commit comments

Comments
 (0)