Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 83 additions & 6 deletions docs/spec/v1beta3/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -1103,15 +1103,92 @@ When `.spec.type` is set to `nats`, the controller will publish the payload of
an [Event](events.md#event-structure) on the [NATS Subject](https://docs.nats.io/nats-concepts/subjects) provided in the
[Channel](#channel) field, using the server specified in the [Address](#address) field.

This Provider type can optionally use the [Secret reference](#secret-reference) to
authenticate to the NATS server using [Username/Password](https://docs.nats.io/using-nats/developer/connecting/userpass).
The credentials must be specified in [the `username`](#username-example) and `password` fields of the Secret.
Alternatively, NATS also supports passing the credentials with [the server URL](https://docs.nats.io/using-nats/developer/connecting/userpass#connecting-with-a-user-password-in-the-url). In this case the `address` should be provided through a
This Provider type supports multiple authentication methods through the [Secret reference](#secret-reference):

1. **User Credentials (JWT)** - [NATS 2.0+ authentication](https://docs.nats.io/running-a-nats-service/configuration/securing_nats/auth_intro/jwt) using JWT tokens and NKeys (recommended)
2. **NKeys** - [NKey-based authentication](https://docs.nats.io/running-a-nats-service/configuration/securing_nats/auth_intro/nkey_auth) using public-key cryptography
3. **Username/Password** - [Basic authentication](https://docs.nats.io/using-nats/developer/connecting/userpass) (legacy)

**Authentication Priority:** If multiple authentication methods are provided in the Secret, the controller will use them in the following priority order:
1. User credentials (`creds` field) - takes precedence
2. NKey seed (`nkey` field) - used if credentials are not provided
3. Username/Password (`username` and `password` fields) - used if neither credentials nor NKey are provided

When [Basic authentication](https://docs.nats.io/using-nats/developer/connecting/userpass) (legacy) username/password method is used NATS supports passing the credentials with [the server URL](https://docs.nats.io/using-nats/developer/connecting/userpass#connecting-with-a-user-password-in-the-url). In this case the `address` should be provided through a
Secret reference.

Additionally if using credentials, the User must have [authorization](https://docs.nats.io/running-a-nats-service/configuration/securing_nats/authorization) to publish on the Subject provided.
The authenticated User must have [authorization](https://docs.nats.io/running-a-nats-service/configuration/securing_nats/authorization) to publish on the [Subject](https://docs.nats.io/nats-concepts/subjects) provided.

###### NATS with User Credentials (JWT) Example

To configure a Provider for NATS using user credentials (recommended for NATS 2.0+), create a Secret with the
`creds` field containing the contents of your `.creds` file, and add a `nats` Provider with the associated
[Secret reference](#secret-reference).

```yaml
---
apiVersion: notification.toolkit.fluxcd.io/v1beta3
kind: Provider
metadata:
name: nats-provider
namespace: desired-namespace
spec:
type: nats
address: <NATS Server URL>
channel: <Subject>
secretRef:
name: nats-provider-creds
---
apiVersion: v1
kind: Secret
metadata:
name: nats-provider-creds
namespace: desired-namespace
stringData:
creds: |
-----BEGIN NATS USER JWT-----
eyJ0eXAiOiJqd3QiLCJhbGciOiJlZDI1NTE5...
------END NATS USER JWT------

************************* IMPORTANT *************************
NKEY Seed printed below can be used to sign and prove identity.
NKEYs are sensitive and should be treated as secrets.

-----BEGIN USER NKEY SEED-----
SUAGMJH5XLGZKQQWAWKRZJIGMOU4HPFUYLXJMXOO5NLFEO2OOQJ5LPRDPM
------END USER NKEY SEED------
```

###### NATS with NKey Authentication Example

To configure a Provider for NATS using NKey authentication, create a Secret with the
`nkey` field containing your NKey seed, and add a `nats` Provider with the associated
[Secret reference](#secret-reference).

```yaml
---
apiVersion: notification.toolkit.fluxcd.io/v1beta3
kind: Provider
metadata:
name: nats-provider
namespace: desired-namespace
spec:
type: nats
address: <NATS Server URL>
channel: <Subject>
secretRef:
name: nats-provider-creds
---
apiVersion: v1
kind: Secret
metadata:
name: nats-provider-creds
namespace: desired-namespace
stringData:
nkey: SUAGMJH5XLGZKQQWAWKRZJIGMOU4HPFUYLXJMXOO5NLFEO2OOQJ5LPRDPM
```

###### NATS with Username/Password Credentials Example
###### NATS with Username/Password Example

To configure a Provider for NATS authenticating with Username/Password, create a Secret with the
`username` and `password` fields set, and add a `nats` Provider with the associated
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ require (
github.com/ktrysmt/go-bitbucket v0.9.87
github.com/microsoft/azure-devops-go-api/azuredevops/v6 v6.0.1
github.com/nats-io/nats.go v1.46.1
github.com/nats-io/nkeys v0.4.11
github.com/onsi/gomega v1.38.2
github.com/sethvargo/go-limiter v1.0.0
github.com/slok/go-http-metrics v0.13.0
Expand Down Expand Up @@ -159,7 +160,6 @@ require (
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
github.com/nats-io/nkeys v0.4.11 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/package-url/packageurl-go v0.1.1 // indirect
Expand Down
10 changes: 9 additions & 1 deletion internal/notifier/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,15 @@ func dataDogNotifierFunc(opts notifierOptions) (Interface, error) {
}

func natsNotifierFunc(opts notifierOptions) (Interface, error) {
return NewNATS(opts.URL, opts.Channel, opts.Username, opts.Password)
// Extract credentials from secret data
// Keys: "creds" for user credentials file, "nkey" for nkey seed
var credsData, nkeySeed []byte
if opts.SecretData != nil {
credsData = opts.SecretData["creds"]
nkeySeed = opts.SecretData["nkey"]
}

return NewNATS(opts.URL, opts.Channel, opts.Username, opts.Password, credsData, nkeySeed)
}

func gitHubNotifierFunc(opts notifierOptions) (Interface, error) {
Expand Down
75 changes: 64 additions & 11 deletions internal/notifier/nats.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (

eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1"
"github.com/nats-io/nats.go"
"github.com/nats-io/nkeys"
"sigs.k8s.io/controller-runtime/pkg/log"
)

Expand All @@ -37,26 +38,69 @@ type (
}

natsClient struct {
server string
username string
password string
server string
authFn func() (nats.Option, func(), error)
}
)

func NewNATS(server string, subject string, username string, password string) (*NATS, error) {
// NewNATS creates a new NATS notifier with support for multiple authentication methods.
//
// Authentication methods (in priority order):
// 1. User Credentials (JWT + NKey): Pass the .creds file content via credsData parameter
// 2. NKey: Pass the NKey seed via nkeySeed parameter
// 3. Username/Password: Pass via username and password parameters
//
// Parameters:
// - server: NATS server URL (e.g., "nats://localhost:4222")
// - subject: NATS subject to publish events to
// - username: Username for basic authentication (optional)
// - password: Password for basic authentication (optional)
// - credsData: User credentials file content (JWT + NKey) for NATS 2.0+ authentication (optional)
// - nkeySeed: NKey seed for NKey-based authentication (optional)
//
// Returns an error if server or subject is empty.
func NewNATS(server string, subject string, username string, password string, credsData []byte, nkeySeed []byte) (*NATS, error) {
if server == "" {
return nil, errors.New("NATS server (address) cannot be empty")
}
if subject == "" {
return nil, errors.New("NATS subject (channel) cannot be empty")
}

client := &natsClient{server: server}

// Set up authentication function based on provided credentials
// Authentication priority: user credentials (JWT), nkey, username/password
if len(credsData) > 0 {
client.authFn = func() (nats.Option, func(), error) {
return nats.UserCredentialBytes(credsData), nil, nil
}
} else if len(nkeySeed) > 0 {
client.authFn = func() (nats.Option, func(), error) {
kp, err := nkeys.FromSeed(nkeySeed)
if err != nil {
return nil, nil, fmt.Errorf("error parsing nkey seed: %w", err)
}
pubKey, err := kp.PublicKey()
if err != nil {
kp.Wipe()
return nil, nil, fmt.Errorf("error getting public key from nkey: %w", err)
}
// Create signature callback
sigCB := func(nonce []byte) ([]byte, error) {
return kp.Sign(nonce)
}
return nats.Nkey(pubKey, sigCB), kp.Wipe, nil
}
} else if username != "" && password != "" {
client.authFn = func() (nats.Option, func(), error) {
return nats.UserInfo(username, password), nil, nil
}
}

return &NATS{
subject: subject,
client: &natsClient{
server: server,
username: username,
password: password,
},
client: client,
}, nil
}

Expand Down Expand Up @@ -85,8 +129,17 @@ func (n *NATS) Post(ctx context.Context, event eventv1.Event) error {

func (n *natsClient) publish(ctx context.Context, subject string, eventPayload []byte) (err error) {
opts := []nats.Option{nats.Name("NATS Provider Publisher")}
if n.username != "" && n.password != "" {
opts = append(opts, nats.UserInfo(n.username, n.password))

// Apply authentication if configured
if n.authFn != nil {
authOpt, cleanup, err := n.authFn()
if err != nil {
return err
}
if cleanup != nil {
defer cleanup()
}
opts = append(opts, authOpt)
}

nc, err := nats.Connect(n.server, opts...)
Expand Down
78 changes: 75 additions & 3 deletions internal/notifier/nats_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"testing"

eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1"
"github.com/nats-io/nats.go"
. "github.com/onsi/gomega"
)

Expand All @@ -17,10 +18,14 @@ func TestNewNATS(t *testing.T) {
server string
username string
password string
credsData []byte
nkeySeed []byte
expectedErr error
expectedSubject string
expectedUsername string
expectedPassword string
expectedCreds bool
expectedNkey bool
}{
{
name: "empty subject is not allowed",
Expand Down Expand Up @@ -54,13 +59,51 @@ func TestNewNATS(t *testing.T) {
expectedUsername: "user",
expectedPassword: "pass",
},
{
name: "credentials file data is stored",
subject: "test",
server: "nats",
credsData: []byte("credentials-content"),
expectedSubject: "test",
expectedCreds: true,
},
{
name: "nkey seed data is stored",
subject: "test",
server: "nats",
nkeySeed: []byte("SUAGMJH5XLGZKQQWAWKRZJIGMOU4HPFUYLXJMXOO5NLFEO2OOQJ5LPRDPM"),
expectedSubject: "test",
expectedNkey: true,
},
// Priority tests: creds > nkey > username/password
{
name: "creds takes priority over nkey and username/password",
subject: "test",
server: "nats",
username: "user",
password: "pass",
credsData: []byte("credentials-content"),
nkeySeed: []byte("SUAGMJH5XLGZKQQWAWKRZJIGMOU4HPFUYLXJMXOO5NLFEO2OOQJ5LPRDPM"),
expectedSubject: "test",
expectedCreds: true,
},
{
name: "nkey takes priority over username/password",
subject: "test",
server: "nats",
username: "user",
password: "pass",
nkeySeed: []byte("SUAGMJH5XLGZKQQWAWKRZJIGMOU4HPFUYLXJMXOO5NLFEO2OOQJ5LPRDPM"),
expectedSubject: "test",
expectedNkey: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)

provider, err := NewNATS(tt.server, tt.subject, tt.username, tt.password)
provider, err := NewNATS(tt.server, tt.subject, tt.username, tt.password, tt.credsData, tt.nkeySeed)
if tt.expectedErr != nil {
g.Expect(err).To(Equal(tt.expectedErr))
g.Expect(provider).To(BeNil())
Expand All @@ -75,8 +118,37 @@ func TestNewNATS(t *testing.T) {
g.Expect(client).NotTo(BeNil())

g.Expect(client.server).To(Equal(tt.server))
g.Expect(client.username).To(Equal(tt.expectedUsername))
g.Expect(client.password).To(Equal(tt.expectedPassword))

// Verify authFn returns valid option and correct auth type
if client.authFn != nil {
opt, cleanup, err := client.authFn()
g.Expect(err).To(BeNil(), "authFn should not return error")
g.Expect(opt).NotTo(BeNil(), "authFn should return a valid option")
if cleanup != nil {
defer cleanup()
}

// Apply option to verify which auth method was selected
var opts nats.Options
g.Expect(opt(&opts)).To(Succeed())

if tt.expectedCreds {
g.Expect(opts.UserJWT).NotTo(BeNil(), "creds auth should set UserJWT nats.Options field")
} else if tt.expectedNkey {
g.Expect(opts.Nkey).NotTo(BeEmpty(), "nkey auth should set Nkey nats.Options field")
g.Expect(opts.SignatureCB).NotTo(BeNil(), "nkey auth should set SignatureCB nats.Options field")
} else if tt.expectedUsername != "" {
g.Expect(opts.User).To(Equal(tt.expectedUsername), "username/password auth should set User nats.Options field")
g.Expect(opts.Password).To(Equal(tt.expectedPassword), "username/password auth should set Password nats.Options field")
}
} else {
// When no auth is provided, authFn should be nil
g.Expect(client.authFn).To(BeNil(), "authFn should be nil when no auth is provided")
g.Expect(tt.expectedCreds).To(BeFalse())
g.Expect(tt.expectedNkey).To(BeFalse())
g.Expect(tt.expectedUsername).To(BeEmpty())
g.Expect(tt.expectedPassword).To(BeEmpty())
}
}
})
}
Expand Down
Loading