Skip to content

Instantly share code, notes, and snippets.

@renevo
Last active August 5, 2023 01:19
Show Gist options
  • Save renevo/8fa7282d441b46752c9151644a2e911a to your computer and use it in GitHub Desktop.
Save renevo/8fa7282d441b46752c9151644a2e911a to your computer and use it in GitHub Desktop.
NATS Embeded JWT

Generating keys and jwts

This is basically how nsc creates an operator account + system account + system user really only need to keep track of the seeds, as they can be used to make public/private keys

JWT claims aren't that bad to make, just a lot of options in there basic operation is that:

  • you create an operator KP and claims
  • operator has a signing KP
  • you create an account KP and claims, and sign the account with the operator signing KP
  • account has a signing KP
  • you create a user KP and claims, and sign the user with the account signing KP
  • operator claims are signed with the operator KP (self signed)
			operatorKP, _ := nkeys.CreateOperator()
			operatorPK, _ := operatorKP.PublicKey()
			operatorK, _ := operatorKP.PrivateKey()

			operatorSeed, _ := operatorKP.Seed()
			parsedKP, _ := nkeys.FromSeed(operatorSeed)
			parsedPK, _ := parsedKP.PublicKey()
			parsedK, _ := parsedKP.PrivateKey()

			fmt.Fprintf(os.Stdout, "Parsed Public: %q; Previous: %q; Equal: %v\n", parsedPK, operatorPK, parsedPK == operatorPK)
			fmt.Fprintf(os.Stdout, "Parsed Privat: %q; Previous: %q; Equal: %v\n", string(parsedK), string(operatorK), string(parsedK) == string(operatorK))
			fmt.Fprintln(os.Stdout)

			var operatorClaims = jwt.NewOperatorClaims(operatorPK)
			operatorClaims.Name = "conservator"
			operatorSigningKeyKP, _ := nkeys.CreateOperator()
			operatorSigningKeyPK, _ := operatorSigningKeyKP.PublicKey()
			operatorClaims.SigningKeys.Add(operatorSigningKeyPK)

			// create system account
			systemAccountKP, _ := nkeys.CreateAccount()
			systemAccountPK, _ := systemAccountKP.PublicKey()
			accountSignerKP, _ := nkeys.CreateAccount()
			accountSignerPK, _ := accountSignerKP.PublicKey()

			systemAccountClaims := jwt.NewAccountClaims(systemAccountPK)
			systemAccountClaims.Name = "SYS"
			systemAccountClaims.SigningKeys.Add(accountSignerPK)
			systemAccountClaims.Exports = jwt.Exports{
				&jwt.Export{
					Name:                 "account-monitoring-services",
					Subject:              "$SYS.REQ.ACCOUNT.*.*",
					Type:                 jwt.Service,
					ResponseType:         jwt.ResponseTypeStream,
					AccountTokenPosition: 4,
					Info: jwt.Info{
						Description: "Custom account made by conservator",
						InfoURL:     "https://github.com/renevo/conservator",
					},
				},
				&jwt.Export{
					Name:                 "account-monitoring-streams",
					Subject:              "$SYS.ACCOUNT.*.>",
					Type:                 jwt.Stream,
					AccountTokenPosition: 3,
					Info: jwt.Info{
						Description: "Custom account made by conservator",
						InfoURL:     "https://github.com/renevo/conservator",
					},
				},
			}

			systemAccountToken, _ := systemAccountClaims.Encode(operatorSigningKeyKP)

			systemUserKP, _ := nkeys.CreateUser()
			systemUserPK, _ := systemUserKP.PublicKey()

			systemUserClaims := jwt.NewUserClaims(systemUserPK)
			systemUserClaims.Name = "sys"
			systemUserClaims.IssuerAccount = systemAccountPK
			systemUserToken, _ := systemUserClaims.Encode(accountSignerKP)

			operatorClaims.SystemAccount = systemAccountPK

			token, _ := operatorClaims.Encode(operatorKP)
			fmt.Fprintf(os.Stdout, "Operator Token: %s\n\n", token)
			fmt.Fprintf(os.Stdout, "System Account Token: %s\n\n", systemAccountToken)
			fmt.Fprintf(os.Stdout, "System User Token: %s\n\n", systemUserToken)

Nats client configuration

Using a user token/seed you can apply this client option:

 nats.UserJWTAndSeed("<jwt token>", "<nkey seed>")

Nats Server configuration

First you will need the Operator JWT

		token, err := jwt.DecodeOperatorClaims("<operator jwt>")
		if err != nil {
			return errors.Wrap(err, "invalid operator token")
		}

		serverOpts.TrustedOperators = append(serverOpts.TrustedOperators, token)

I use a temporary account resolver, but you can use whatever you need, there are a few builtin to the nats-server code:

type placeHolderAR struct {
	accounts sync.Map
}

func (ar *placeHolderAR) Fetch(name string) (string, error) {
	act, found := ar.accounts.Load(name)
	if !found {
		return "", server.ErrMissingAccount
	}

	return act.(string), nil
}

func (ar *placeHolderAR) Store(name, jwt string) error {
	ar.accounts.Store(name, jwt)
	return nil
}

func (ar *placeHolderAR) IsReadOnly() bool {
	return true
}

func (ar *placeHolderAR) Start(srv *server.Server) error {
	// assuming that the nats server wants us to connect/preload stuff here...
	return nil
}

func (ar *placeHolderAR) IsTrackingUpdate() bool {
	return false
}

func (ar *placeHolderAR) Reload() error {
	return nil
}

func (ar *placeHolderAR) Close() {

}

Then for the system account:

    ar := &placeHolderAR{}

		token, err := jwt.DecodeAccountClaims("<system jwt>")
		if err != nil {
			return errors.Wrap(err, "invalid system token")
		}

		serverOpts.SystemAccount = token.Subject
		if err := ar.Store(token.Subject, "<system jwt>"); err != nil {
			return errors.Wrap(err, "failed to store system token in account store")
		}

		token, err := jwt.DecodeAccountClaims("<normal account token>")
		if err != nil {
			return errors.Wrap(err, "invalid agent token")
		}

		if err := ar.Store(token.Subject, "<normal account token>"); err != nil {
			return errors.Wrap(err, "failed to store account token in account store")
		}

    serverOpts.AccountResolver = ar

Cluster, Leaf Nodes, and Gateways all need to be configured individually as well with accounts/users and be setup, but won't cover that hear right now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment