Skip to content

Commit 6738260

Browse files
authored
Merge pull request #873 from ripienaar/check_credentials
Adds server check credentials that can monitor cred files
2 parents 3cc9bc3 + e2671b6 commit 6738260

File tree

3 files changed

+169
-7
lines changed

3 files changed

+169
-7
lines changed

cli/server_check_command.go

Lines changed: 75 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,19 @@ import (
1818
"encoding/json"
1919
"fmt"
2020
"net/url"
21+
"os"
2122
"regexp"
2223
"strconv"
2324
"time"
2425

2526
"github.com/choria-io/fisk"
2627
"github.com/nats-io/jsm.go"
2728
"github.com/nats-io/jsm.go/api"
29+
"github.com/nats-io/jwt/v2"
2830
"github.com/nats-io/nats-server/v2/server"
2931
"github.com/nats-io/nats.go"
3032
"github.com/nats-io/natscli/monitor"
33+
"github.com/nats-io/nkeys"
3134
)
3235

3336
type SrvCheckCmd struct {
@@ -86,10 +89,14 @@ type SrvCheckCmd struct {
8689
msgRegexp *regexp.Regexp
8790
msgBodyAsTs bool
8891

89-
kvBucket string
90-
kvValuesCrit int
91-
kvValuesWarn int
92-
kvKey string
92+
kvBucket string
93+
kvValuesCrit int
94+
kvValuesWarn int
95+
kvKey string
96+
credentialValidityCrit time.Duration
97+
credentialValidityWarn time.Duration
98+
credentialRequiresExpire bool
99+
credential string
93100
}
94101

95102
func configureServerCheckCommand(srv *fisk.CmdClause) {
@@ -170,6 +177,12 @@ func configureServerCheckCommand(srv *fisk.CmdClause) {
170177
kv.Flag("values-critical", "Critical threshold for number of values in the bucket").Default("-1").IntVar(&c.kvValuesCrit)
171178
kv.Flag("values-warn", "Warning threshold for number of values in the bucket").Default("-1").IntVar(&c.kvValuesWarn)
172179
kv.Flag("key", "Requires a key to have any non-delete value set").StringVar(&c.kvKey)
180+
181+
cred := check.Command("credential", "Checks the validity of a NATS credential file").Action(c.checkCredentialAction)
182+
cred.Flag("credential", "The file holding the NATS credential").Required().StringVar(&c.credential)
183+
cred.Flag("validity-warn", "Warning threshold for time before expiry").DurationVar(&c.credentialValidityWarn)
184+
cred.Flag("validity-critical", "Critical threshold for time before expiry").DurationVar(&c.credentialValidityCrit)
185+
cred.Flag("require-expiry", "Requires the credential to have expiry set").Default("true").BoolVar(&c.credentialRequiresExpire)
173186
}
174187

175188
var (
@@ -986,3 +999,61 @@ func (c *SrvCheckCmd) checkConnection(_ *fisk.ParseContext) error {
986999

9871000
return nil
9881001
}
1002+
1003+
func (c *SrvCheckCmd) checkCredential(check *monitor.Result) error {
1004+
ok, err := fileAccessible(c.credential)
1005+
if err != nil {
1006+
check.Critical("credential not accessible: %v", err)
1007+
return nil
1008+
}
1009+
1010+
if !ok {
1011+
check.Critical("credential not accessible")
1012+
return nil
1013+
}
1014+
1015+
cb, err := os.ReadFile(c.credential)
1016+
if err != nil {
1017+
check.Critical("credential not accessible: %v", err)
1018+
return nil
1019+
}
1020+
1021+
token, err := nkeys.ParseDecoratedJWT(cb)
1022+
if err != nil {
1023+
check.Critical("invalid credential: %v", err)
1024+
return nil
1025+
}
1026+
1027+
claims, err := jwt.Decode(token)
1028+
if err != nil {
1029+
check.Critical("invalid credential: %v", err)
1030+
}
1031+
1032+
now := time.Now().UTC().Unix()
1033+
cd := claims.Claims()
1034+
until := cd.Expires - now
1035+
crit := int64(c.credentialValidityCrit.Seconds())
1036+
warn := int64(c.credentialValidityWarn.Seconds())
1037+
1038+
check.Pd(&monitor.PerfDataItem{Help: "Expiry time in seconds", Name: "expiry", Value: float64(until), Warn: float64(warn), Crit: float64(crit), Unit: "s"})
1039+
1040+
switch {
1041+
case cd.Expires == 0 && c.credentialRequiresExpire:
1042+
check.Critical("never expires")
1043+
case c.credentialValidityCrit > 0 && (until <= crit):
1044+
check.Critical("expires sooner than %s", f(c.credentialValidityCrit))
1045+
case c.credentialValidityWarn > 0 && (until <= warn):
1046+
check.Warn("expires sooner than %s", f(c.credentialValidityWarn))
1047+
default:
1048+
check.Ok("expires in %s", time.Unix(cd.Expires, 0).UTC())
1049+
}
1050+
1051+
return nil
1052+
}
1053+
1054+
func (c *SrvCheckCmd) checkCredentialAction(_ *fisk.ParseContext) error {
1055+
check := &monitor.Result{Name: "Credential", Check: "credential", OutFile: checkRenderOutFile, NameSpace: opts.PrometheusNamespace, RenderFormat: checkRenderFormat}
1056+
defer check.GenericExit()
1057+
1058+
return c.checkCredential(check)
1059+
}

cli/server_check_command_test.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -765,6 +765,97 @@ func TestCheckVarz(t *testing.T) {
765765
})
766766
}
767767

768+
func TestCheckCredential(t *testing.T) {
769+
noExpiry := `-----BEGIN NATS USER JWT-----
770+
eyJ0eXAiOiJKV1QiLCJhbGciOiJlZDI1NTE5LW5rZXkifQ.eyJqdGkiOiJBSUdIM0I2TEFGQkMzNktaSFJCSFI1QVZaTVFHQkdDS0NRTlNXRFBMN0U1NE5SM0I1SkxRIiwiaWF0IjoxNjk1MzY5NjU1LCJpc3MiOiJBRFFCT1haQTZaWk5MMko0VFpZNTZMUVpUN1FCVk9DNDVLVlQ3UDVNWkZVWU1LSVpaTUdaSE02QSIsIm5hbWUiOiJib2IiLCJzdWIiOiJVQkhPVDczREVGN1dZWUZUS1ZVSDZNWDNFUUVZSlFWWUNBRUJXUFJaSDNYR0E2WDdLRDNGUkFYSCIsIm5hdHMiOnsicHViIjp7fSwic3ViIjp7fSwic3VicyI6LTEsImRhdGEiOi0xLCJwYXlsb2FkIjotMSwidHlwZSI6InVzZXIiLCJ2ZXJzaW9uIjoyfX0.kGsxvI3NNNp60unItd-Eo1Yw6B9T3rBOeq7lvRY_klP5yTaBZwhCTKUNYdr_n2HNkCNB44fyW2_pmBhDki_CDQ
771+
------END NATS USER JWT------
772+
773+
************************* IMPORTANT *************************
774+
NKEY Seed printed below can be used to sign and prove identity.
775+
NKEYs are sensitive and should be treated as secrets.
776+
777+
-----BEGIN USER NKEY SEED-----
778+
SUAIQJDZJGYOJN4NBOLYRRENCNTPXZ7PPVQW7RWEXWJUNBAFDRPDO27JWA
779+
------END USER NKEY SEED------
780+
781+
*************************************************************`
782+
783+
expires2100 := `-----BEGIN NATS USER JWT-----
784+
eyJ0eXAiOiJKV1QiLCJhbGciOiJlZDI1NTE5LW5rZXkifQ.eyJleHAiOjQxMDI0NDQ4MDAsImp0aSI6IlhRQkNTUUo3M0c3STRWR0JVUUNNQjdKRVlDWlVNUzdLUzJPU0Q1Skk3WjY0NEE0TU40SUEiLCJpYXQiOjE2OTUzNzA4OTcsImlzcyI6IkFERU5CTlBZSUwzTklXVkxCMjJVUU5FR0NMREhGSllNSUxEVEFQSlk1SFlQV05LQVZQNzJXREFSIiwibmFtZSI6ImJvYiIsInN1YiI6IlVCTTdYREtRUzRRQVBKUEFCSllWSU5RR1lETko2R043MjZNQ01DV0VZRDJTTU9GQVZOQ1E1M09IIiwibmF0cyI6eyJwdWIiOnt9LCJzdWIiOnt9LCJzdWJzIjotMSwiZGF0YSI6LTEsInBheWxvYWQiOi0xLCJ0eXBlIjoidXNlciIsInZlcnNpb24iOjJ9fQ.3ytewtkFoRLKNeRJjPGOyNWeeQKqKdfHmyRL2ofaUiqj_OoN2LAmg_Ms2zpU-A_2xAiUH7VsMIRJxw1cx3bwAg
785+
------END NATS USER JWT------
786+
787+
************************* IMPORTANT *************************
788+
NKEY Seed printed below can be used to sign and prove identity.
789+
NKEYs are sensitive and should be treated as secrets.
790+
791+
-----BEGIN USER NKEY SEED-----
792+
SUAKYITMHPMSYUGPNQBLLPGOPFQN44XNCGXHNSHLJJVMD3IKYGBOLAI7TI
793+
------END USER NKEY SEED------
794+
795+
*************************************************************`
796+
797+
writeCred := func(t *testing.T, cred string) string {
798+
tf, err := os.CreateTemp("", "")
799+
assertNoError(t, err)
800+
801+
tf.Write([]byte(cred))
802+
tf.Close()
803+
804+
return tf.Name()
805+
}
806+
807+
t.Run("no expiry", func(t *testing.T) {
808+
cmd := &SrvCheckCmd{}
809+
cmd.credential = writeCred(t, noExpiry)
810+
defer func(f string) { os.Remove(f) }(cmd.credential)
811+
812+
cmd.credentialRequiresExpire = true
813+
814+
check := &monitor.Result{}
815+
assertNoError(t, cmd.checkCredential(check))
816+
assertListEquals(t, check.Criticals, "never expires")
817+
assertListIsEmpty(t, check.Warnings)
818+
819+
cmd.credential = writeCred(t, expires2100)
820+
defer func(f string) { os.Remove(f) }(cmd.credential)
821+
822+
check = &monitor.Result{}
823+
assertNoError(t, cmd.checkCredential(check))
824+
assertListIsEmpty(t, check.Criticals)
825+
assertListIsEmpty(t, check.Warnings)
826+
assertListEquals(t, check.OKs, "expires in 2100-01-01 00:00:00 +0000 UTC")
827+
})
828+
829+
t.Run("critical", func(t *testing.T) {
830+
cmd := &SrvCheckCmd{}
831+
cmd.credential = writeCred(t, expires2100)
832+
833+
defer os.Remove(cmd.credential)
834+
835+
check := &monitor.Result{}
836+
cmd.credentialValidityCrit = 100 * 24 * 365 * time.Hour
837+
838+
assertNoError(t, cmd.checkCredential(check))
839+
assertListEquals(t, check.Criticals, "expires sooner than 100y0d0h0m0s")
840+
assertListIsEmpty(t, check.Warnings)
841+
assertListIsEmpty(t, check.OKs)
842+
})
843+
844+
t.Run("warning", func(t *testing.T) {
845+
cmd := &SrvCheckCmd{}
846+
cmd.credential = writeCred(t, expires2100)
847+
defer os.Remove(cmd.credential)
848+
849+
check := &monitor.Result{}
850+
cmd.credentialValidityWarn = 100 * 24 * 365 * time.Hour
851+
852+
assertNoError(t, cmd.checkCredential(check))
853+
assertListEquals(t, check.Warnings, "expires sooner than 100y0d0h0m0s")
854+
assertListIsEmpty(t, check.Criticals)
855+
assertListIsEmpty(t, check.OKs)
856+
})
857+
858+
}
768859
func TestCheckJSZ(t *testing.T) {
769860
cmd := &SrvCheckCmd{}
770861

cli/util_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,13 @@ func assertListIsEmpty(t *testing.T, list []string) {
4747
}
4848
}
4949

50-
func assertListEquals(t *testing.T, list []string, crits ...string) {
50+
func assertListEquals(t *testing.T, list []string, vals ...string) {
5151
t.Helper()
5252

5353
sort.Strings(list)
54-
sort.Strings(crits)
54+
sort.Strings(vals)
5555

56-
if !cmp.Equal(list, crits) {
56+
if !cmp.Equal(list, vals) {
5757
t.Fatalf("invalid items: %v", list)
5858
}
5959
}

0 commit comments

Comments
 (0)