Repository: nats-io/nsc
Pull request #307 (2020-10-22 02:26:32)

Added env variable NATS_CA to provide root ca option to nats
When set the value will be used for nats connections.
When not set NSC_HOME/pkix/rootcas-nats.pem will be used, when present.
Else, default to system trust store.

Signed-off-by: Matthias Hanel <mh@synadia.com>

Tried manually as setting env variables in tests and generating certs was harder than it should be.
Manually tried without env set, with rootcas-nats.pem absent/present/NSC_HOME changed.
 
Below is debug output from a manual test to assure that createDefaultToolOptions picks up the value set.

```
> nsc sub f
Error: x509: certificate signed by unknown authority
...

> export NSC_ROOTCAS_NATS=/Users/matthiashanel/test/rootCA.pem; nsc env
╭──────────────────────────────────────────────────────────────────────────────────────────────────────╮
│                                           NSC Environment                                            │
├────────────────────┬─────┬───────────────────────────────────────────────────────────────────────────┤
│ Setting            │ Set │ Effective Value                                                           │
├────────────────────┼─────┼───────────────────────────────────────────────────────────────────────────┤
│ $NSC_CWD_ONLY      │ No  │ If set, default operator/account from cwd only                            │
│ $NSC_NO_GIT_IGNORE │ No  │ If set, no .gitignore files written                                       │
│ $NKEYS_PATH        │ No  │ ~/.nkeys                                                                  │
│ $NSC_HOME          │ No  │ ~/.nsc                                                                    │
│ Config             │     │ ~/.nsc/nsc.json                                                           │
│ $NSC_ROOTCAS_NATS  │ Yes │ If set, root CAs in the referenced file will be used for nats connections │
│                    │     │ If not set, when preset, $NSC_HOME/pkix/rootcas-nats.pem will be used     │
├────────────────────┼─────┼───────────────────────────────────────────────────────────────────────────┤
│ From CWD           │     │ No                                                                        │
│ Stores Dir         │     │ ~/test/demo/nsc                                                           │
│ Default Operator   │     │ JWT_DEMO                                                                  │
│ Default Account    │     │ SYS                                                                       │
│ Root CAs to trust  │     │ File: /Users/matthiashanel/test/rootCA.pem                                │
╰────────────────────┴─────┴───────────────────────────────────────────────────────────────────────────╯

> export NSC_ROOTCAS_NATS=/Users/matthiashanel/test/rootCA.pem; nsc sub f
Listening on [f]
^C
> cat tls.cfg
listen: localhost:4222
tls {
	cert_file: "./server-cert.pem"
	key_file:  "./server-key.pem"
}

>
```

matthiashanel: > filepath, here the environment is specifying a directory, with a well known filename, I think we need to not introduce any conv

Was following a suggestion from phil
matthiashanel: ```
> nsc env
╭──────────────────────────────────────────────────────────────────────────────────────────────────────╮
│                                           NSC Environment                                            │
├────────────────────┬─────┬───────────────────────────────────────────────────────────────────────────┤
│ Setting            │ Set │ Effective Value                                                           │
├────────────────────┼─────┼───────────────────────────────────────────────────────────────────────────┤
│ $NSC_CWD_ONLY      │ No  │ If set, default operator/account from cwd only                            │
│ $NSC_NO_GIT_IGNORE │ No  │ If set, no .gitignore files written                                       │
│ $NKEYS_PATH        │ No  │ ~/.nkeys                                                                  │
│ $NSC_HOME          │ No  │ ~/.nsc                                                                    │
│ Config             │     │ ~/.nsc/nsc.json                                                           │
│ $NSC_ROOTCAS_NATS  │ Yes │ If set, root CAs in the referenced file will be used for nats connections │
│                    │     │ If not set, will default to the system trust store                        │
├────────────────────┼─────┼───────────────────────────────────────────────────────────────────────────┤
│ From CWD           │     │ No                                                                        │
│ Stores Dir         │     │ ~/test/demo/nsc                                                           │
│ Default Operator   │     │ JWT_DEMO                                                                  │
│ Default Account    │     │ SYS                                                                       │
│ Root CAs to trust  │     │ File: foo                                                                 │
╰────────────────────┴─────┴───────────────────────────────────────────────────────────────────────────╯

> unset NSC_ROOTCAS_NATS
> nsc env
╭──────────────────────────────────────────────────────────────────────────────────────────────────────╮
│                                           NSC Environment                                            │
├────────────────────┬─────┬───────────────────────────────────────────────────────────────────────────┤
│ Setting            │ Set │ Effective Value                                                           │
├────────────────────┼─────┼───────────────────────────────────────────────────────────────────────────┤
│ $NSC_CWD_ONLY      │ No  │ If set, default operator/account from cwd only                            │
│ $NSC_NO_GIT_IGNORE │ No  │ If set, no .gitignore files written                                       │
│ $NKEYS_PATH        │ No  │ ~/.nkeys                                                                  │
│ $NSC_HOME          │ No  │ ~/.nsc                                                                    │
│ Config             │     │ ~/.nsc/nsc.json                                                           │
│ $NSC_ROOTCAS_NATS  │ No  │ If set, root CAs in the referenced file will be used for nats connections │
│                    │     │ If not set, will default to the system trust store                        │
├────────────────────┼─────┼───────────────────────────────────────────────────────────────────────────┤
│ From CWD           │     │ No                                                                        │
│ Stores Dir         │     │ ~/test/demo/nsc                                                           │
│ Default Operator   │     │ JWT_DEMO                                                                  │
│ Default Account    │     │ SYS                                                                       │
│ Root CAs to trust  │     │ Default: System Trust Store                                               │
╰────────────────────┴─────┴───────────────────────────────────────────────────────────────────────────╯


```

Patch:
diff --git a/cmd/defaults.go b/cmd/defaults.go
index b8669dc..98da922 100644
--- a/cmd/defaults.go
+++ b/cmd/defaults.go
@@ -25,9 +25,9 @@ import (
 	"strings"
 	"time"
 
-	"github.com/nats-io/jwt"
-
 	"github.com/mitchellh/go-homedir"
+	"github.com/nats-io/jwt"
+	"github.com/nats-io/nats.go"
 	"github.com/nats-io/nsc/cmd/store"
 )
 
@@ -35,6 +35,7 @@ import (
 const NscHomeEnv = "NSC_HOME"
 const NscCwdOnlyEnv = "NSC_CWD_ONLY"
 const NscNoGitIgnoreEnv = "NSC_NO_GIT_IGNORE"
+const NscRootCasNatsEnv = "NATS_CA"
 
 type ToolConfig struct {
 	ContextConfig
@@ -47,6 +48,8 @@ var toolName = strings.ReplaceAll(filepath.Base(os.Args[0]), ".exe", "")
 var config ToolConfig
 var toolHome string
 var homeEnv string
+var rootCAsNats nats.Option // Will be skipped, when nil and passed to a connection
+var rootCAsFile string
 
 func SetToolName(name string) {
 	toolName = name
diff --git a/cmd/env.go b/cmd/env.go
index 39880a4..e7d114a 100644
--- a/cmd/env.go
+++ b/cmd/env.go
@@ -98,6 +98,9 @@ func (p *SetContextParams) PrintEnv(cmd *cobra.Command) {
 	table.AddRow("$"+store.NKeysPathEnv, envSet(store.NKeysPathEnv), AbbrevHomePaths(store.GetKeysDir()))
 	table.AddRow("$"+homeEnv, envSet(homeEnv), AbbrevHomePaths(toolHome))
 	table.AddRow("Config", "", AbbrevHomePaths(conf.configFile()))
+	table.AddRow("$"+NscRootCasNatsEnv, envSet(NscRootCasNatsEnv),
+		"If set, root CAs in the referenced file will be used for nats connections")
+	table.AddRow("", "", "If not set, will default to the system trust store")
 	table.AddSeparator()
 	r := conf.StoreRoot
 	if r == "" {
@@ -107,5 +110,12 @@ func (p *SetContextParams) PrintEnv(cmd *cobra.Command) {
 	table.AddRow("Stores Dir", "", AbbrevHomePaths(r))
 	table.AddRow("Default Operator", "", conf.Operator)
 	table.AddRow("Default Account", "", conf.Account)
+	caFile := rootCAsFile
+	if caFile == "" {
+		caFile = "Default: System Trust Store"
+	} else {
+		caFile = "File: " + caFile
+	}
+	table.AddRow("Root CAs to trust", "", caFile)
 	cmd.Println(table.Render())
 }
diff --git a/cmd/pubtool.go b/cmd/pubtool.go
index c26bbc8..93102f0 100644
--- a/cmd/pubtool.go
+++ b/cmd/pubtool.go
@@ -106,9 +106,8 @@ func (p *PubParams) Validate(ctx ActionCtx) error {
 }
 
 func (p *PubParams) Run(ctx ActionCtx) (store.Status, error) {
-	opts := createDefaultToolOptions("nsc_pub", ctx)
-	opts = append(opts, nats.UserCredentials(p.credsPath))
-	nc, err := nats.Connect(strings.Join(p.natsURLs, ", "), opts...)
+	nc, err := nats.Connect(strings.Join(p.natsURLs, ", "),
+		createDefaultToolOptions("nsc_pub", ctx, nats.UserCredentials(p.credsPath))...)
 	if err != nil {
 		return nil, err
 	}
diff --git a/cmd/pull.go b/cmd/pull.go
index 46c35a7..bb0a516 100644
--- a/cmd/pull.go
+++ b/cmd/pull.go
@@ -246,7 +246,7 @@ func (p *PullParams) Run(ctx ActionCtx) (store.Status, error) {
 			subR.AddError("failed to obtain system user: %v", err)
 			return r, nil
 		}
-		nc, err := nats.Connect(url, opt, nats.Name("nsc-client"))
+		nc, err := nats.Connect(url, createDefaultToolOptions("nsc_pull", ctx, opt)...)
 		if err != nil {
 			subR.AddError("failed to connect to %s: %v", url, err)
 			return r, nil
diff --git a/cmd/push.go b/cmd/push.go
index ca42ab0..3a8fa10 100644
--- a/cmd/push.go
+++ b/cmd/push.go
@@ -376,7 +376,7 @@ func (p *PushCmdParams) Run(ctx ActionCtx) (store.Status, error) {
 			r.AddError("error obtaining system account user: %v", err)
 			return r, nil
 		}
-		nc, err := nats.Connect(p.ASU, opt, nats.Name("nsc-client"))
+		nc, err := nats.Connect(p.ASU, createDefaultToolOptions("nsc_push", ctx, opt)...)
 		if err != nil {
 			r.AddError("failed to connect: %v", err)
 			return r, nil
diff --git a/cmd/replytool.go b/cmd/replytool.go
index 755129c..d4a45f3 100644
--- a/cmd/replytool.go
+++ b/cmd/replytool.go
@@ -111,9 +111,8 @@ func (p *RepParams) Validate(ctx ActionCtx) error {
 }
 
 func (p *RepParams) Run(ctx ActionCtx) (store.Status, error) {
-	opts := createDefaultToolOptions("nscreply", ctx)
-	opts = append(opts, nats.UserCredentials(p.credsPath))
-	nc, err := nats.Connect(strings.Join(p.natsURLs, ","), opts...)
+	nc, err := nats.Connect(strings.Join(p.natsURLs, ","),
+		createDefaultToolOptions("nsc_reply", ctx, nats.UserCredentials(p.credsPath))...)
 	if err != nil {
 		return nil, err
 	}
diff --git a/cmd/reqtool.go b/cmd/reqtool.go
index 7f1b9c3..c44c6d5 100644
--- a/cmd/reqtool.go
+++ b/cmd/reqtool.go
@@ -113,9 +113,8 @@ func (p *ReqParams) Validate(ctx ActionCtx) error {
 }
 
 func (p *ReqParams) Run(ctx ActionCtx) (store.Status, error) {
-	opts := createDefaultToolOptions("nsc_req", ctx)
-	opts = append(opts, nats.UserCredentials(p.credsPath))
-	nc, err := nats.Connect(strings.Join(p.natsURLs, ", "), opts...)
+	nc, err := nats.Connect(strings.Join(p.natsURLs, ", "),
+		createDefaultToolOptions("nsc_req", ctx, nats.UserCredentials(p.credsPath))...)
 	if err != nil {
 		return nil, err
 	}
diff --git a/cmd/root.go b/cmd/root.go
index ccc25e7..aa11d13 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -21,9 +21,11 @@ import (
 	"io"
 	"os"
 	"path/filepath"
+	"strings"
 
 	"github.com/mitchellh/go-homedir"
 	cli "github.com/nats-io/cliprompts/v2"
+	"github.com/nats-io/nats.go"
 	"github.com/nats-io/nkeys"
 	"github.com/nats-io/nsc/cmd/store"
 	"github.com/spf13/cobra"
@@ -155,6 +157,10 @@ func SetEnvOptions() {
 	if _, ok := os.LookupEnv(NscCwdOnlyEnv); ok {
 		NscCwdOnly = true
 	}
+	if f, ok := os.LookupEnv(NscRootCasNatsEnv); ok {
+		rootCAsFile = strings.TrimSpace(f)
+		rootCAsNats = nats.RootCAs(rootCAsFile)
+	}
 }
 
 func init() {
diff --git a/cmd/rtttool.go b/cmd/rtttool.go
index acc3aba..ffa0147 100644
--- a/cmd/rtttool.go
+++ b/cmd/rtttool.go
@@ -103,9 +103,8 @@ func (p *RttParams) Validate(ctx ActionCtx) error {
 }
 
 func (p *RttParams) Run(ctx ActionCtx) (store.Status, error) {
-	opts := createDefaultToolOptions("nsc_rtt", ctx)
-	opts = append(opts, nats.UserCredentials(p.credsPath))
-	nc, err := nats.Connect(strings.Join(p.natsURLs, ", "), opts...)
+	nc, err := nats.Connect(strings.Join(p.natsURLs, ", "),
+		createDefaultToolOptions("nsc_rtt", ctx, nats.UserCredentials(p.credsPath))...)
 	if err != nil {
 		return nil, err
 	}
diff --git a/cmd/subtool.go b/cmd/subtool.go
index d4445f4..4b47a99 100644
--- a/cmd/subtool.go
+++ b/cmd/subtool.go
@@ -115,9 +115,8 @@ func (p *SubParams) Validate(ctx ActionCtx) error {
 }
 
 func (p *SubParams) Run(ctx ActionCtx) (store.Status, error) {
-	opts := createDefaultToolOptions("nscsub", ctx)
-	opts = append(opts, nats.UserCredentials(p.credsPath))
-	nc, err := nats.Connect(strings.Join(p.natsURLs, ", "), opts...)
+	nc, err := nats.Connect(strings.Join(p.natsURLs, ", "),
+		createDefaultToolOptions("nsc_sub", ctx, nats.UserCredentials(p.credsPath))...)
 	if err != nil {
 		return nil, err
 	}
diff --git a/cmd/tool.go b/cmd/tool.go
index 6d69951..9439a7d 100644
--- a/cmd/tool.go
+++ b/cmd/tool.go
@@ -44,13 +44,14 @@ func init() {
 	GetRootCmd().AddCommand(toolCmd)
 }
 
-func createDefaultToolOptions(name string, ctx ActionCtx) []nats.Option {
+func createDefaultToolOptions(name string, ctx ActionCtx, o ...nats.Option) []nats.Option {
 	connectTimeout := 5 * time.Second
 	totalWait := 10 * time.Minute
 	reconnectDelay := 2 * time.Second
 
 	opts := []nats.Option{nats.Name(name)}
 	opts = append(opts, nats.Timeout(connectTimeout))
+	opts = append(opts, rootCAsNats)
 	opts = append(opts, nats.ReconnectWait(reconnectDelay))
 	opts = append(opts, nats.MaxReconnects(int(totalWait/reconnectDelay)))
 	opts = append(opts, nats.DisconnectErrHandler(func(nc *nats.Conn, err error) {
@@ -71,6 +72,7 @@ func createDefaultToolOptions(name string, ctx ActionCtx) []nats.Option {
 		}
 		ctx.CurrentCmd().Printf("Exiting, no servers available, or connection closed")
 	}))
+	opts = append(opts, o...)
 	return opts
 }
 


Test patch:
diff --git a/cmd/tools_test.go b/cmd/tools_test.go
index 746e2c0..ba86a25 100644
--- a/cmd/tools_test.go
+++ b/cmd/tools_test.go
@@ -112,7 +112,7 @@ func TestSub(t *testing.T) {
 	}()
 
 	// wait for client
-	ts.WaitForClient(t, "nscsub", 1, 60*time.Second)
+	ts.WaitForClient(t, "nsc_sub", 1, 60*time.Second)
 
 	// create a conn to the server
 	creds := ts.KeyStore.CalcUserCredsPath("A", "U")
@@ -213,7 +213,7 @@ func TestReply(t *testing.T) {
 	}()
 
 	// wait for client
-	ts.WaitForClient(t, "nscreply", 1, 60*time.Second)
+	ts.WaitForClient(t, "nsc_reply", 1, 60*time.Second)
 
 	// create a conn to the server
 	creds := ts.KeyStore.CalcUserCredsPath("A", "U")