forked from stoggi/sshrimp
-
Notifications
You must be signed in to change notification settings - Fork 1
/
config.go
463 lines (424 loc) 路 12.7 KB
/
config.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
package config
import (
"errors"
"fmt"
"net/url"
"os"
"reflect"
"strconv"
"strings"
"time"
"github.com/AlecAivazis/survey/v2"
"github.com/BurntSushi/toml"
"github.com/kballard/go-shellquote"
)
// Agent config for the sshrimp-agent agent
type Agent struct {
ProviderURL string
ClientID string
ClientSecret string
BrowserCommand []string
Socket string
}
// CertificateAuthority config for the sshrimp-ca lambda
type CertificateAuthority struct {
AccountID int
Regions []string
FunctionName string
KeyAlias string
ForceCommandRegex string
SourceAddressRegex string
UsernameRegex string
UsernameClaim string
ValidAfterOffset string
ValidBeforeOffset string
Extensions []string
ProvisioningUser string
// IdentityProviderURI string
// IdentityProviderClientID string
}
// SSHrimp main configuration struct for sshrimp-agent and sshrimp-ca
type SSHrimp struct {
Agent Agent
CertificateAuthority CertificateAuthority
}
// List of supported regions for the config wizard
var supportedAwsRegions = []string{
"ap-east-1",
"ap-northeast-1",
"ap-northeast-2",
"ap-south-1",
"ap-southeast-1",
"ap-southeast-2",
"ca-central-1",
"eu-central-1",
"eu-north-1",
"eu-west-1",
"eu-west-2",
"eu-west-3",
"me-south-1",
"sa-east-1",
"us-east-1",
"us-east-2",
"us-west-1",
"us-west-2",
}
var supportedExtensions = []string{
"no-agent-forwarding",
"no-port-forwarding",
"no-pty",
"no-user-rc",
"no-x11-forwarding",
"permit-agent-forwarding",
"permit-port-forwarding",
"permit-pty",
"permit-user-rc",
"permit-x11-forwarding",
}
// NewSSHrimp returns SSHrimp
func NewSSHrimp() *SSHrimp {
return &SSHrimp{}
}
// NewSSHrimpWithDefaults returns SSHrimp with defaults already set
func NewSSHrimpWithDefaults() *SSHrimp {
sshrimp := SSHrimp{
Agent{
ProviderURL: "https://accounts.google.com",
Socket: "/tmp/sshrimp.sock",
},
CertificateAuthority{
FunctionName: "sshrimp",
KeyAlias: "alias/sshrimp",
ForceCommandRegex: "^$",
SourceAddressRegex: "^$",
UsernameRegex: `^(.*)@example\.com$`,
UsernameClaim: "email",
ValidAfterOffset: "-5m",
ValidBeforeOffset: "+12h",
Extensions: []string{
"permit-agent-forwarding",
"permit-port-forwarding",
"permit-pty",
"permit-user-rc",
"no-x11-forwarding",
},
ProvisioningUser: "",
},
}
return &sshrimp
}
// DefaultPath of the sshrimp config file
var DefaultPath = "./sshrimp.toml"
// EnvVarName is the optional environment variable that if set overrides DefaultPath
var EnvVarName = "SSHRIMP_CONFIG"
// GetPath returns the default sshrimp config file path taking into account EnvVarName
func GetPath() string {
if configPathFromEnv, ok := os.LookupEnv(EnvVarName); ok && configPathFromEnv != "" {
return configPathFromEnv
}
return DefaultPath
}
func validateInt(val interface{}) error {
if str, ok := val.(string); ok {
if _, err := strconv.Atoi(str); err != nil {
return err
}
} else {
return fmt.Errorf("expected type string got %v", reflect.TypeOf(val).Name())
}
return nil
}
func validateURL(val interface{}) error {
if str, ok := val.(string); ok {
if _, err := url.ParseRequestURI(str); err != nil {
return err
}
} else {
return fmt.Errorf("expected type string got %v", reflect.TypeOf(val).Name())
}
return nil
}
func validateDuration(val interface{}) error {
if str, ok := val.(string); ok {
if _, err := time.ParseDuration(str); err != nil {
return err
}
} else {
return fmt.Errorf("expected type string got %v", reflect.TypeOf(val).Name())
}
return nil
}
func validateAlias(val interface{}) error {
if str, ok := val.(string); ok {
if !strings.HasPrefix(str, "alias/") {
return errors.New("KMS alias must begin with alias/")
}
} else {
return fmt.Errorf("expected type string got %v", reflect.TypeOf(val).Name())
}
return nil
}
func certificateAuthorityQuestions(config *SSHrimp) []*survey.Question {
defaultAccountID := ""
if config.CertificateAuthority.AccountID > 0 {
defaultAccountID = strconv.Itoa(config.CertificateAuthority.AccountID)
}
return []*survey.Question{
{
Name: "AccountID",
Prompt: &survey.Input{
Message: "AWS Account ID:",
Default: defaultAccountID,
Help: "12 Digit account ID. You could get this by running `aws sts get-caller-identity`",
},
Validate: survey.ComposeValidators(
survey.Required,
validateInt,
survey.MaxLength(12),
survey.MinLength(12),
),
},
{
Name: "Regions",
Prompt: &survey.MultiSelect{
Message: "AWS Region:",
Default: config.CertificateAuthority.Regions,
Help: "Select multiple regions for high availability. Each region gets it's own Lambda function and KMS key.",
Options: supportedAwsRegions,
PageSize: 10,
},
Validate: survey.Required,
},
{
Name: "FunctionName",
Prompt: &survey.Input{
Message: "Lambda Function Name:",
Help: "The sshrimp certificate authority lambda will have this name.",
Default: config.CertificateAuthority.FunctionName,
},
Validate: survey.Required,
},
{
Name: "KeyAlias",
Prompt: &survey.Input{
Message: "KMS Key Alias:",
Help: "A name beginning with 'alias/' to easily refer to KMS keys in IAM policies and configuration files.",
Default: config.CertificateAuthority.KeyAlias,
},
Validate: survey.ComposeValidators(
survey.Required,
validateAlias,
),
},
{
Name: "UsernameClaim",
Prompt: &survey.Input{
Message: "Username claim in JWT",
Help: "Which claim in the JWT should be used as the username. See https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims",
Default: config.CertificateAuthority.UsernameClaim,
},
Validate: survey.Required,
},
{
Name: "UsernameRegex",
Prompt: &survey.Input{
Message: "Username regular expression",
Help: "A regular expression to validate the username present in the identity token. The first matching group will be used as the username enforced in the certificate.",
Default: config.CertificateAuthority.UsernameRegex,
},
Validate: survey.Required,
},
{
Name: "ForceCommandRegex",
Prompt: &survey.Input{
Message: "ForceCommand regular expression:",
Help: "A regular expression to validate the force command supplied by the user, but enforced in the certificate. See https://man.openbsd.org/sshd_config#ForceCommand",
Default: config.CertificateAuthority.ForceCommandRegex,
},
Validate: survey.Required,
},
{
Name: "SourceAddressRegex",
Prompt: &survey.Input{
Message: "Source IP address regular expression",
Help: "A regular expression to validate the source IP address supplied by the user, but enforced in the certificate.",
Default: config.CertificateAuthority.SourceAddressRegex,
},
Validate: survey.Required,
},
{
Name: "ValidAfterOffset",
Prompt: &survey.Input{
Message: "A time.now() offset for valid_after",
Help: "The amount to add to time.now() that the certificate will be valid FROM.",
Default: config.CertificateAuthority.ValidAfterOffset,
},
Validate: survey.ComposeValidators(
survey.Required,
validateDuration,
),
},
{
Name: "ValidBeforeOffset",
Prompt: &survey.Input{
Message: "A time.now() offset for valid_before",
Help: "The amount to add to time.now() that the certificate will be valid TO.",
Default: config.CertificateAuthority.ValidBeforeOffset,
},
Validate: survey.ComposeValidators(
survey.Required,
validateDuration,
),
},
{
Name: "Extensions",
Prompt: &survey.MultiSelect{
Message: "Certificate extensions",
Help: "Extensions to be added to the certificate, see https://man.openbsd.org/ssh-keygen#CERTIFICATES",
Default: config.CertificateAuthority.Extensions,
Options: supportedExtensions,
PageSize: 10,
},
Validate: survey.Required,
},
{
Name: "ProvisioningUser",
Prompt: &survey.Input{
Message: "Provisioning User for creating new users",
Help: "An existing user account on the machine which can be used for adding the user if it doesn't exist. Added as a Principal to all signed certs.",
Default: config.CertificateAuthority.ProvisioningUser,
},
Validate: survey.Required,
},
}
}
func agentQuestions(config *SSHrimp) []*survey.Question {
return []*survey.Question{
{
Name: "ProviderURL",
Prompt: &survey.Input{
Message: "OpenIDConnect Provider URL:",
Default: config.Agent.ProviderURL,
Help: "Get this from your OIDC provider. For example Google's is https://accounts.google.com.",
},
Validate: survey.ComposeValidators(survey.Required, validateURL),
},
{
Name: "ClientID",
Prompt: &survey.Input{
Message: "OpenIDConnect Client ID:",
Default: config.Agent.ClientID,
Help: "Get this from your OIDC provider. For example Google uses the format 1234-0a1b2bc3.apps.googleusercontent.com",
},
Validate: survey.Required,
},
{
Name: "ClientSecret",
Prompt: &survey.Input{
Message: "OpenIDConnect Client Secret (only if required):",
Default: config.Agent.ClientSecret,
Help: "Google requires the Client Secret even when using PKCE. Most OpenIDConnect provdiders don't. Read more about PKCE: https://tools.ietf.org/html/rfc7636",
},
},
{
Name: "Socket",
Prompt: &survey.Input{
Message: "sshrimp-agent socket:",
Default: config.Agent.Socket,
Help: "Path of the socket for the sshrimp-agent to listen on. Create a unique one for each instance of sshrimp-agent.",
},
Validate: survey.Required,
},
}
}
func browserCommandQuestions(config *SSHrimp) []*survey.Question {
return []*survey.Question{
{
Name: "BrowserCommand",
Prompt: &survey.Input{
Message: "Command to open a browser:",
Default: shellquote.Join(config.Agent.BrowserCommand...),
Help: "Optionally {} will be substituted with the URL to open.",
},
Validate: survey.Required,
},
}
}
func configFileQuestions(configPath string) []*survey.Question {
return []*survey.Question{
{
Name: "ConfigPath",
Prompt: &survey.Input{
Message: "File path to write the new config:",
Default: configPath,
Help: "Set environment variable SSHRIMP_CONFIG to this path if different from ./sshrimp.toml",
},
Validate: survey.Required,
},
}
}
func (c *SSHrimp) Read(configPath string) error {
_, err := toml.DecodeFile(configPath, c)
return err
}
func (c *SSHrimp) Write(configPath string) error {
// Create the new config file
configFile, err := os.Create(configPath)
if err != nil {
return err
}
defer configFile.Close()
// Encode the configuration values as a TOML file
encoder := toml.NewEncoder(configFile)
if err := encoder.Encode(c); err != nil {
return err
}
return nil
}
// Wizard launches a interactive question/answer terminal prompt to create a config file
func Wizard(configPath string, config *SSHrimp) (string, error) {
// Create a new config that doesn't have any default values, otherwise survey appends to the defaults.
newConfig := NewSSHrimp()
if err := survey.Ask(certificateAuthorityQuestions(config), &newConfig.CertificateAuthority); err != nil {
return "", err
}
if err := survey.Ask(agentQuestions(config), &newConfig.Agent); err != nil {
return "", err
}
//duplicating the necessary fields from Agent Config over to the CertificateAuthority Config
// newConfig.CertificateAuthority.IdentityProviderURI = strings.Replace(newConfig.Agent.ProviderURL, "https://", "", 1)
// newConfig.CertificateAuthority.IdentityProviderClientID = newConfig.Agent.ClientID
// Ask BrowserCommand separately so we can store it as a []string, currently not supported by survey.
var browserCommand string
if err := survey.Ask(browserCommandQuestions(config), &browserCommand); err != nil {
return "", err
}
// Split the command by sh rules using shellquote. The command is stored as a slice of arguments.
words, err := shellquote.Split(browserCommand)
if err != nil {
return "", err
}
newConfig.Agent.BrowserCommand = words
// Confirm config file path, and keep prompting if exists and user chooses not to overwrite
var overwriteIfExists = false
for !overwriteIfExists {
if err := survey.Ask(configFileQuestions(configPath), &configPath); err != nil {
return "", err
}
if _, err := os.Stat(configPath); err == nil {
// File exists, confirm to be overwritten
if err := survey.AskOne(&survey.Confirm{
Message: "File exists, overwrite?",
Default: false,
}, &overwriteIfExists); err != nil {
return "", err
}
} else {
// File doesn't exist, break and save the file
break
}
}
// Write the new configuration to a file
newConfig.Write(configPath)
return configPath, nil
}