Skip to content

Commit bfd0906

Browse files
Merge pull request #32 from exoscale/mtls
Add support for client side TLS
2 parents 9376569 + 08459a3 commit bfd0906

File tree

8 files changed

+402
-42
lines changed

8 files changed

+402
-42
lines changed

cli/apiconfig.go

+9
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,14 @@ type APIAuth struct {
1919
Params map[string]string `json:"params"`
2020
}
2121

22+
// TLSConfig contains the TLS setup for the HTTP client
23+
type TLSConfig struct {
24+
InsecureSkipVerify bool `json:"insecure" mapstructure:"insecure"`
25+
Cert string `json:"cert"`
26+
Key string `json:"key"`
27+
CACert string `json:"ca_cert" mapstructure:"ca_cert"`
28+
}
29+
2230
// APIProfile contains account-specific API information
2331
type APIProfile struct {
2432
Headers map[string]string `json:"headers,omitempty"`
@@ -33,6 +41,7 @@ type APIConfig struct {
3341
Base string `json:"base"`
3442
SpecFiles []string `json:"spec_files,omitempty" mapstructure:"spec_files,omitempty"`
3543
Profiles map[string]*APIProfile `json:"profiles,omitempty" mapstructure:",omitempty"`
44+
TLS *TLSConfig `json:"tls,omitempty" mapstructure:",omitempty"`
3645
}
3746

3847
// Save the API configuration to disk.

cli/cli.go

+41-15
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,18 @@ import (
1919
"github.com/mattn/go-colorable"
2020
"github.com/mattn/go-isatty"
2121
"github.com/spf13/cobra"
22+
"github.com/spf13/pflag"
2223
"github.com/spf13/viper"
2324
)
2425

2526
// Root command (entrypoint) of the CLI.
2627
var Root *cobra.Command
2728

29+
// GlobalFlags contains all the fixed up front flags
30+
// This allows us to parse them before we hand over control
31+
// to cobra
32+
var GlobalFlags *pflag.FlagSet
33+
2834
// Cache is used to store temporary data between runs.
2935
var Cache *viper.Viper
3036

@@ -135,16 +141,6 @@ func Init(name string, version string) {
135141
PersistentPreRun: func(cmd *cobra.Command, args []string) {
136142
settings := viper.AllSettings()
137143
LogDebug("Configuration: %v", settings)
138-
139-
if viper.GetBool("rsh-insecure") {
140-
if t, ok := http.DefaultTransport.(*http.Transport); ok {
141-
LogWarning("Disabling TLS security checks")
142-
if t.TLSClientConfig == nil {
143-
t.TLSClientConfig = &tls.Config{}
144-
}
145-
t.TLSClientConfig.InsecureSkipVerify = true
146-
}
147-
}
148144
},
149145
Run: func(cmd *cobra.Command, args []string) {
150146
generic(http.MethodGet, args[0], args[1:])
@@ -317,6 +313,14 @@ Not after (expires): %s (%s)
317313
}
318314
Root.AddCommand(linkCmd)
319315

316+
GlobalFlags = pflag.NewFlagSet("eager-flags", pflag.ContinueOnError)
317+
GlobalFlags.ParseErrorsWhitelist.UnknownFlags = true
318+
// GlobalFlags are 'hidden', don't print anything on error
319+
GlobalFlags.Usage = func() {}
320+
// Ensure parsing doesn't stop if the help flag is set
321+
// (help seems to be special cased from ParseErrorsWhitelist.UnknownFlags)
322+
GlobalFlags.BoolP("help", "h", false, "")
323+
320324
AddGlobalFlag("rsh-verbose", "v", "Enable verbose log output", false, false)
321325
AddGlobalFlag("rsh-output-format", "o", "Output format [auto, json, yaml]", "auto", false)
322326
AddGlobalFlag("rsh-filter", "f", "Filter / project results using JMESPath Plus", "", false)
@@ -328,6 +332,9 @@ Not after (expires): %s (%s)
328332
AddGlobalFlag("rsh-profile", "p", "API auth profile", "default", false)
329333
AddGlobalFlag("rsh-no-cache", "", "Disable HTTP cache", false, false)
330334
AddGlobalFlag("rsh-insecure", "", "Disable SSL verification", false, false)
335+
AddGlobalFlag("rsh-client-cert", "", "Path to a PEM encoded client certificate", "", false)
336+
AddGlobalFlag("rsh-client-key", "", "Path to a PEM encoded private key", "", false)
337+
AddGlobalFlag("rsh-ca-cert", "", "Path to a PEM encoded CA cert", "", false)
331338

332339
initAPIConfig()
333340
}
@@ -427,15 +434,34 @@ func Run() {
427434
if !strings.HasPrefix(arg, "-") {
428435
args = append(args, arg)
429436
}
437+
}
430438

431-
// Try to detect if verbose mode is enabled via a flag.
432-
if arg == "-v" || arg == "--rsh-verbose" {
433-
enableVerbose = true
439+
// Because we may be doing HTTP calls before cobra has parsed the flags
440+
// we parse the GlobalFlags here and already set some config values
441+
// to ensure they are available
442+
if err := GlobalFlags.Parse(os.Args[1:]); err != nil {
443+
if err != pflag.ErrHelp {
444+
panic(err)
434445
}
435446
}
447+
if verbose, _ := GlobalFlags.GetBool("rsh-verbose"); verbose {
448+
viper.Set("rsh-verbose", true)
449+
}
450+
if insecure, _ := GlobalFlags.GetBool("rsh-insecure"); insecure {
451+
viper.Set("rsh-insecure", true)
452+
}
453+
if cert, _ := GlobalFlags.GetString("rsh-client-cert"); cert != "" {
454+
viper.Set("rsh-client-cert", cert)
455+
}
456+
if key, _ := GlobalFlags.GetString("rsh-client-key"); key != "" {
457+
viper.Set("rsh-client-key", key)
458+
}
459+
if caCert, _ := GlobalFlags.GetString("rsh-ca-cert"); caCert != "" {
460+
viper.Set("rsh-ca-cert", caCert)
461+
}
436462

437-
// Now that flags are parsed we can enable verbose mode if requested.
438-
if enableVerbose || viper.GetBool("rsh-verbose") {
463+
// Now that global flags are parsed we can enable verbose mode if requested.
464+
if viper.GetBool("rsh-verbose") {
439465
enableVerbose = true
440466
}
441467

cli/flag.go

+7
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,25 @@ func AddGlobalFlag(name, short, description string, defaultValue interface{}, mu
1616
case bool:
1717
if multi {
1818
flags.BoolSliceP(name, short, viper.Get(name).([]bool), description)
19+
GlobalFlags.BoolSliceP(name, short, viper.Get(name).([]bool), description)
1920
} else {
2021
flags.BoolP(name, short, viper.GetBool(name), description)
22+
GlobalFlags.BoolP(name, short, viper.GetBool(name), description)
2123
}
2224
case int, int16, int32, int64, uint16, uint32, uint64:
2325
if multi {
2426
flags.IntSliceP(name, short, viper.Get(name).([]int), description)
27+
GlobalFlags.IntSliceP(name, short, viper.Get(name).([]int), description)
2528
} else {
2629
flags.IntP(name, short, viper.GetInt(name), description)
30+
GlobalFlags.IntP(name, short, viper.GetInt(name), description)
2731
}
2832
case float32, float64:
2933
if multi {
3034
panic(fmt.Errorf("unsupported float slice param"))
3135
} else {
3236
flags.Float64P(name, short, viper.GetFloat64(name), description)
37+
GlobalFlags.Float64P(name, short, viper.GetFloat64(name), description)
3338
}
3439
default:
3540
if multi {
@@ -40,8 +45,10 @@ func AddGlobalFlag(name, short, description string, defaultValue interface{}, mu
4045
viper.Set(name, v)
4146
}
4247
flags.StringSliceP(name, short, v.([]string), description)
48+
GlobalFlags.StringSliceP(name, short, v.([]string), description)
4349
} else {
4450
flags.StringP(name, short, fmt.Sprintf("%v", viper.Get(name)), description)
51+
GlobalFlags.StringP(name, short, fmt.Sprintf("%v", viper.Get(name)), description)
4552
}
4653
}
4754

cli/interactive.go

+80-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"github.com/AlecAivazis/survey/v2"
99
"github.com/AlecAivazis/survey/v2/terminal"
1010
"github.com/spf13/cobra"
11+
"github.com/spf13/viper"
1112
)
1213

1314
var surveyOpts = []survey.AskOpt{}
@@ -291,11 +292,83 @@ func askAddProfile(a asker, config *APIConfig) {
291292
askEditProfile(a, name, config.Profiles[name])
292293
}
293294

295+
func askTLSConfig(a asker, config *APIConfig) {
296+
if config.TLS == nil {
297+
config.TLS = &TLSConfig{}
298+
}
299+
300+
for {
301+
options := make([]string, 0, 7)
302+
303+
if config.TLS.InsecureSkipVerify {
304+
options = append(options, "Delete insecure")
305+
} else {
306+
options = append(options, "Set insecure")
307+
}
308+
309+
if config.TLS.Cert == "" {
310+
options = append(options, "Set certificate")
311+
} else {
312+
options = append(options, "Edit certificate", "Delete certificate")
313+
}
314+
315+
if config.TLS.Key == "" {
316+
options = append(options, "Set key")
317+
} else {
318+
options = append(options, "Edit key", "Delete key")
319+
}
320+
321+
if config.TLS.CACert == "" {
322+
options = append(options, "Set CA certificate")
323+
} else {
324+
options = append(options, "Edit CA certificate", "Delete CA certificate")
325+
}
326+
327+
options = append(options, "Finished with TLS configuration")
328+
329+
switch choice := a.askSelect("Select TLS configuration options", options, nil, ""); choice {
330+
case "Delete insecure":
331+
config.TLS.InsecureSkipVerify = false
332+
case "Set insecure":
333+
config.TLS.InsecureSkipVerify = true
334+
case "Set certificate":
335+
config.TLS.Cert = a.askInput("Certificate path", "", false, "")
336+
case "Edit certificate":
337+
config.TLS.Cert = a.askInput("Certificate path", config.TLS.Cert, false, "")
338+
case "Delete certificate":
339+
config.TLS.Cert = ""
340+
case "Set key":
341+
config.TLS.Key = a.askInput("Key path", "", false, "")
342+
case "Edit key":
343+
config.TLS.Key = a.askInput("Key path", config.TLS.Key, false, "")
344+
case "Delete key":
345+
config.TLS.Key = ""
346+
case "Set CA certificate":
347+
config.TLS.CACert = a.askInput("CA Certificate path", "", false, "")
348+
case "Edit CA certificate":
349+
config.TLS.CACert = a.askInput("CA Certificate path", config.TLS.CACert, false, "")
350+
case "Delete CA certificate":
351+
config.TLS.CACert = ""
352+
case "Finished with TLS configuration":
353+
return
354+
}
355+
}
356+
}
357+
294358
func askInitAPI(a asker, cmd *cobra.Command, args []string) {
295359
var config *APIConfig = configs[args[0]]
296360

297361
if config == nil {
298-
config = &APIConfig{name: args[0], Profiles: map[string]*APIProfile{}}
362+
config = &APIConfig{
363+
name: args[0],
364+
Profiles: map[string]*APIProfile{},
365+
TLS: &TLSConfig{
366+
InsecureSkipVerify: viper.GetBool("rsh-insecure"),
367+
Cert: viper.GetString("rsh-client-cert"),
368+
Key: viper.GetString("rsh-client-key"),
369+
CACert: viper.GetString("rsh-ca-cert"),
370+
},
371+
}
299372
configs[args[0]] = config
300373

301374
// Do an initial setup with a default profile first.
@@ -324,6 +397,10 @@ func askInitAPI(a asker, cmd *cobra.Command, args []string) {
324397
options = append(options, "Edit profile "+k)
325398
}
326399

400+
if (config.TLS != nil) && (*config.TLS != TLSConfig{}) {
401+
options = append(options, "Edit TLS configuration")
402+
}
403+
327404
options = append(options, "Save and exit")
328405

329406
choice := a.askSelect("Select option", options, nil, "")
@@ -336,6 +413,8 @@ func askInitAPI(a asker, cmd *cobra.Command, args []string) {
336413
case strings.HasPrefix(choice, "Edit profile"):
337414
profile := strings.SplitN(choice, " ", 3)[2]
338415
askEditProfile(a, profile, config.Profiles[profile])
416+
case choice == "Edit TLS configuration":
417+
askTLSConfig(a, config)
339418
case choice == "Save and exit":
340419
config.Save()
341420
return

cli/request.go

+63
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package cli
22

33
import (
4+
"crypto/tls"
5+
"crypto/x509"
46
"fmt"
57
"io/ioutil"
68
"net/http"
@@ -161,6 +163,58 @@ func MakeRequest(req *http.Request, options ...requestOption) (*http.Response, e
161163
}
162164
}
163165

166+
// The assumption is that all Transport implementations eventually use the
167+
// default HTTP transport.
168+
// We can therefore inject the TLS config once here, along with all the other
169+
// config options, instead of modifying all the places where Transports are
170+
// created
171+
LogDebug("Adding TLS configuration")
172+
if t, ok := http.DefaultTransport.(*http.Transport); ok {
173+
if t.TLSClientConfig == nil {
174+
t.TLSClientConfig = &tls.Config{}
175+
}
176+
if config.TLS == nil {
177+
config.TLS = &TLSConfig{}
178+
}
179+
180+
// CLI flags overwrite profile options
181+
if viper.GetBool("rsh-insecure") {
182+
config.TLS.InsecureSkipVerify = true
183+
}
184+
if cert := viper.GetString("rsh-client-cert"); cert != "" {
185+
config.TLS.Cert = cert
186+
}
187+
if key := viper.GetString("rsh-client-key"); key != "" {
188+
config.TLS.Key = key
189+
}
190+
if caCert := viper.GetString("rsh-ca-cert"); caCert != "" {
191+
config.TLS.CACert = caCert
192+
}
193+
194+
if config.TLS.InsecureSkipVerify {
195+
LogWarning("Disabling TLS security checks")
196+
t.TLSClientConfig.InsecureSkipVerify = config.TLS.InsecureSkipVerify
197+
}
198+
if config.TLS.Cert != "" {
199+
cert, err := tls.LoadX509KeyPair(config.TLS.Cert, config.TLS.Key)
200+
if err != nil {
201+
return nil, err
202+
}
203+
t.TLSClientConfig.Certificates = append(t.TLSClientConfig.Certificates, cert)
204+
}
205+
if config.TLS.CACert != "" {
206+
caCert, err := ioutil.ReadFile(config.TLS.CACert)
207+
if err != nil {
208+
return nil, err
209+
}
210+
systemCerts := BestEffortSystemCertPool()
211+
if !systemCerts.AppendCertsFromPEM(caCert) {
212+
return nil, fmt.Errorf("Failed to append CACert %s RootCA list", config.TLS.CACert)
213+
}
214+
t.TLSClientConfig.RootCAs = systemCerts
215+
}
216+
}
217+
164218
if log {
165219
LogDebugRequest(req)
166220
}
@@ -348,3 +402,12 @@ func MakeRequestAndFormat(req *http.Request) {
348402
panic(err)
349403
}
350404
}
405+
406+
// BestEffortSystemCertPool returns system cert pool as best effort, otherwise an empty cert pool
407+
func BestEffortSystemCertPool() *x509.CertPool {
408+
rootCAs, _ := x509.SystemCertPool()
409+
if rootCAs == nil {
410+
return x509.NewCertPool()
411+
}
412+
return rootCAs
413+
}

docs/configuration.md

+15-13
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,21 @@ Global configuration affects all commands and can be set in one of three ways, g
1515

1616
The global options in addition to `--help` and `--version` are:
1717

18-
| Argument | Env Var | Example | Description |
19-
| --------------------------- | ------------------- | ----------------- | -------------------------------------------------------------------------------- |
20-
| `-f`, `--rsh-filter` | `RSH_FILTER` | `body.users[].id` | [JMESPath Plus](https://github.com/danielgtaylor/go-jmespath-plus#readme) filter |
21-
| `-H`, `--rsh-header` | `RSH_HEADER` | `Version:2020-05` | Set a header name/value |
22-
| `--rsh-insecure` | `RSH_INSECURE` | | Disable TLS certificate checks |
23-
| `--rsh-no-cache` | `RSH_NO_CACHE` | | Disable HTTP caching |
24-
| `--rsh-no-paginate` | `RSH_NO_PAGINATE` | | Disable automatic `next` link pagination |
25-
| `-o`, `--rsh-output-format` | `RSH_OUTPUT_FORMAT` | `json` | [Output format](/output.md), defaults to `auto` |
26-
| `-p`, `--rsh-profile` | `RSH_PROFILE` | `testing` | Auth profile name, defaults to `default` |
27-
| `-q`, `--rsh-query` | `RSH_QUERY` | `search=foo` | Set a query parameter |
28-
| `-r`, `--rsh-raw` | `RSH_RAW` | | Raw output for shell processing |
29-
| `-s`, `--rsh-server` | `RSH_SERVER` | `https://foo.com` | Override API server base URL |
30-
| `-v`, `--rsh-verbose` | `RSH_VERBOSE` | | Enable verbose output |
18+
| Argument | Env Var | Example | Description |
19+
| --------------------------- | ------------------- | ------------------- | -------------------------------------------------------------------------------- |
20+
| `-f`, `--rsh-filter` | `RSH_FILTER` | `body.users[].id` | [JMESPath Plus](https://github.com/danielgtaylor/go-jmespath-plus#readme) filter |
21+
| `-H`, `--rsh-header` | `RSH_HEADER` | `Version:2020-05` | Set a header name/value |
22+
| `--rsh-insecure` | `RSH_INSECURE` | | Disable TLS certificate checks |
23+
| `--rsh-client-cert` | `RSH_CLIENT_CERT` | `/etc/ssl/cert.pem` | Path to a PEM encoded client certificate |
24+
| `--rsh-client-key` | `RSH_CLIENT_KEY` | `/etc/ssl/key.pem` | Path to a PEM encoded private key |
25+
| `--rsh-ca-cert` | `RSH_CA_CERT` | `/etc/ssl/ca.pem` | Path to a PEM encoded CA certificate |
26+
| `--rsh-no-paginate` | `RSH_NO_PAGINATE` | | Disable automatic `next` link pagination |
27+
| `-o`, `--rsh-output-format` | `RSH_OUTPUT_FORMAT` | `json` | [Output format](/output.md), defaults to `auto` |
28+
| `-p`, `--rsh-profile` | `RSH_PROFILE` | `testing` | Auth profile name, defaults to `default` |
29+
| `-q`, `--rsh-query` | `RSH_QUERY` | `search=foo` | Set a query parameter |
30+
| `-r`, `--rsh-raw` | `RSH_RAW` | | Raw output for shell processing |
31+
| `-s`, `--rsh-server` | `RSH_SERVER` | `https://foo.com` | Override API server base URL |
32+
| `-v`, `--rsh-verbose` | `RSH_VERBOSE` | | Enable verbose output |
3133

3234
Configuration file keys are the same as long-form arguments without the `--` prefix.
3335

0 commit comments

Comments
 (0)