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
3 changes: 3 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,9 @@ var httpOutputDefaults = map[string]map[string]any{
"NATS": {
"HostPort": "",
"SubjectTemplate": "falco.<priority>.<rule>",
"CredsFile": "",
"NkeySeedFile": "",
"JWTFile": "",
},
"Opsgenie": {
"Region": "us",
Expand Down
3 changes: 3 additions & 0 deletions config_example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,9 @@ loki:
nats:
# hostport: "" # nats://{domain or ip}:{port}, if not empty, NATS output is enabled
# subjecttemplate: "falco.<priority>.<rule>" # template for the subject, tokens <priority> and <rule> will be automatically replaced (default: falco.<priority>.<rule>)
# credsfile: "" # path to NATS .creds file, cannot be combined with jwtfile or nkeyseedfile
# nkeyseedfile: "" # path to NATS NKey seed file, used alone for NKey auth or with jwtfile for JWT auth
# jwtfile: "" # path to NATS JWT file, requires nkeyseedfile
# minimumpriority: "" # minimum priority of event for using this output, order is emergency|alert|critical|error|warning|notice|informational|debug or "" (default)
# mutualtls: false # if true, checkcert flag will be ignored (server cert will always be checked)
# checkcert: true # check if ssl certificate of the output is valid (default: true)
Expand Down
49 changes: 36 additions & 13 deletions docs/outputs/nats.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,40 @@
- [NATS](#nats)
- [Table of content](#table-of-content)
- [Configuration](#configuration)
- [subjecttemplate: "falco.." # template for the subject, tokens and will be automatically replaced (default: falco..)](#subjecttemplate-falco--template-for-the-subject-tokens--and--will-be-automatically-replaced-default-falco)
- [Example of config.yaml](#example-of-configyaml)
- [subjecttemplate: "falco.." # template for the subject, tokens and will be automatically replaced (default: falco..)](#subjecttemplate-falco--template-for-the-subject-tokens--and--will-be-automatically-replaced-default-falco)
- [Example of `config.yaml`](#example-of-configyaml)
- [Additional info](#additional-info)
- [Screenshots](#screenshots)

## Configuration

# subjecttemplate: "falco.<priority>.<rule>" # template for the subject, tokens <priority> and <rule> will be automatically replaced (default: falco.<priority>.<rule>)
| Setting | Env var | Default value | Description |
| ---------------------- | ---------------------- | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `nats.hostport` | `NATS_HOSTPORT` | | `nats://{domain or ip}:{port}`, if not empty, NATS output is **enabled** |
| `nats.subjecttemplate` | `NATS_SUBJECTTEMPLATE` | `falco.<priority>.<rule>`| Template for the subject, tokens `<priority>` and `<rule>` will be automatically replaced |
| `nats.credsfile` | `NATS_CREDSFILE` | `""` | Path to a NATS `.creds` file. This option cannot be combined with `nats.nkeyseedfile` or `nats.jwtfile` |
| `nats.nkeyseedfile` | `NATS_NKEYSEEDFILE` | `""` | Path to a NATS NKey seed file. Can be used alone (NKey auth) or with `nats.jwtfile` (JWT auth) |
| `nats.jwtfile` | `NATS_JWTFILE` | `""` | Path to a NATS JWT file. Requires `nats.nkeyseedfile` |
| `nats.mutualtls` | `NATS_MUTUALTLS` | `false` | Authenticate to the output with TLS, if true, `checkcert` is ignored (server cert will always be checked) |
| `nats.checkcert` | `NATS_CHECKCERT` | `true` | Check if SSL certificate of the output is valid |
| `nats.minimumpriority` | `NATS_MINIMUMPRIORITY` | `""` (= `debug`) | Minimum priority of event for using this output, order is `emergency,alert,critical,error,warning,notice,informational,debug or ""` |

> [!NOTE]
> Env var values override settings from `config.yaml`.

| Setting | Env var | Default value | Description |
| ---------------------- | ---------------------- | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `nats.hostport` | `NATS_HOSTPORT` | | nats://{domain or ip}:{port}, if not empty, NATS output is **enabled** |
| `nats.subjecttemplate` | `NATS_SUBJECTTEMPLATE` | `falco.<priority>.<rule>` | Template for the subject, tokens <priority> and <rule> will be automatically replaced |
| `nats.mutualtls` | `NATS_MUTUALTLS` | `false` | Authenticate to the output with TLS, if true, checkcert flag will be ignored (server cert will always be checked) |
| `nats.checkcert` | `NATS_CHECKCERT` | `true` | Check if ssl certificate of the output is valid |
| `nats.minimumpriority` | `NATS_MINIMUMPRIORITY` | `""` (= `debug`) | Minimum priority of event for using this output, order is `emergency,alert,critical,error,warning,notice,informational,debug or ""` |
<a id="subjecttemplate-falco--template-for-the-subject-tokens--and--will-be-automatically-replaced-default-falco"></a>

> [!NOTE]
The Env var values override the settings from yaml file.
### `subjecttemplate: "falco.<priority>.<rule>" # template for the subject, tokens <priority> and <rule> will be automatically replaced (default: falco.<priority>.<rule>)`

## Example of config.yaml
- Subject tokens:
- `<priority>`: Falco priority (`debug`, `notice`, `warning`, ...)
- `<rule>`: Falco rule name normalized for subjects
- Example result:
- Template: `falco.<priority>.<rule>`
- Event: priority `Debug`, rule `Test rule`
- Subject: `falco.debug.test_rule`

## Example of `config.yaml`

```yaml
nats:
Expand All @@ -38,8 +50,19 @@ nats:
# subjecttemplate: "falco.<priority>.<rule>" # template for the subject, tokens <priority> and <rule> will be automatically replaced (default: falco.<priority>.<rule>)
# mutualtls: false # if true, checkcert flag will be ignored (server cert will always be checked)
# checkcert: true # check if ssl certificate of the output is valid (default: true)
# credsfile: "" # path to NATS .creds file (exclusive with jwtfile/nkeyseedfile)
# nkeyseedfile: "" # path to NATS NKey seed file (alone for NKey auth, or with jwtfile for JWT auth)
# jwtfile: "" # path to NATS JWT file (requires nkeyseedfile)
```

## Additional info

- Supported auth combinations:
- `.creds` mode: set `nats.credsfile` only
- NKey mode: set `nats.nkeyseedfile` only
- JWT mode: set both `nats.jwtfile` and `nats.nkeyseedfile`
- Invalid combinations are rejected at startup and NATS output is disabled.

## Screenshots

No dedicated screenshot for NATS output yet.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ require (
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nats-io/nats-server/v2 v2.11.12 // indirect
github.com/nats-io/nats-streaming-server v0.24.6 // indirect
github.com/nats-io/nkeys v0.4.12 // indirect
github.com/nats-io/nkeys v0.4.12
github.com/nats-io/nuid v1.0.1 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/pierrec/lz4/v4 v4.1.22 // indirect
Expand Down
13 changes: 9 additions & 4 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -317,12 +317,17 @@ func init() {
}

if config.Nats.HostPort != "" {
var err error
natsClient, err = outputs.NewClient("NATS", config.Nats.HostPort, config.Nats.CommonConfig, *initClientArgs)
if err != nil {
if err := outputs.ValidateNatsAuthConfig(config); err != nil {
utils.Log(utils.ErrorLvl, "NATS", err.Error())
config.Nats.HostPort = ""
} else {
outputs.EnabledOutputs = append(outputs.EnabledOutputs, "NATS")
var err error
natsClient, err = outputs.NewClient("NATS", config.Nats.HostPort, config.Nats.CommonConfig, *initClientArgs)
if err != nil {
config.Nats.HostPort = ""
} else {
outputs.EnabledOutputs = append(outputs.EnabledOutputs, "NATS")
}
}
}
Comment thread
mfreeman451 marked this conversation as resolved.

Expand Down
12 changes: 6 additions & 6 deletions outputs/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import (
"net/http"
"net/url"
"os"
"regexp"
"strings"
"sync"

Expand Down Expand Up @@ -153,11 +152,6 @@ type Client struct {

// InitClient returns a new output.Client for accessing the different API.
func NewClient(outputType string, defaultEndpointURL string, cfg types.CommonConfig, params types.InitClientArgs) (*Client, error) {
reg := regexp.MustCompile(`(http|nats)(s?)://.*`)
if !reg.MatchString(defaultEndpointURL) {
utils.Log(utils.ErrorLvl, outputType, "Bad Endpoint")
return nil, ErrClientCreation
}
if _, err := url.ParseRequestURI(defaultEndpointURL); err != nil {
utils.Log(utils.ErrorLvl, outputType, err.Error())
return nil, ErrClientCreation
Expand All @@ -167,6 +161,12 @@ func NewClient(outputType string, defaultEndpointURL string, cfg types.CommonCon
utils.Log(utils.ErrorLvl, outputType, err.Error())
return nil, ErrClientCreation
}
switch endpointURL.Scheme {
case "http", "https", "nats", "tls":
default:
utils.Log(utils.ErrorLvl, outputType, "Bad Endpoint")
return nil, ErrClientCreation
}
return &Client{
cfg: cfg,
OutputType: outputType,
Expand Down
4 changes: 4 additions & 0 deletions outputs/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ func TestNewClient(t *testing.T) {
nc, err := NewClient("test", "http://localhost", types.CommonConfig{CheckCert: true}, *initClientArgs)
require.Nil(t, err)
require.Equal(t, &testClientOutput, nc)

nc, err = NewClient("test", "tls://localhost:4222", types.CommonConfig{CheckCert: true}, *initClientArgs)
require.NoError(t, err)
require.Equal(t, "tls", nc.EndpointURL.Scheme)
}

func TestPost(t *testing.T) {
Expand Down
189 changes: 188 additions & 1 deletion outputs/nats.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@
package outputs

import (
"crypto/tls"
"crypto/x509"
"encoding/json"
"errors"
"fmt"
"os"
"regexp"
"strings"

Expand All @@ -18,6 +23,172 @@ var slugRegExp = regexp.MustCompile("[^a-z0-9]+")

const defaultNatsSubjects = "falco.<priority>.<rule>"

type natsAuthMode uint8

const (
natsAuthModeNone natsAuthMode = iota
natsAuthModeCredsFile
natsAuthModeNkeySeedFile
natsAuthModeJWTAndNkeySeedFile
)

type natsAuthFiles struct {
credsFile string
nkeySeedFile string
jwtFile string
}

func getNatsAuthFiles(config *types.Configuration) natsAuthFiles {
if config == nil {
return natsAuthFiles{}
}

return natsAuthFiles{
credsFile: strings.TrimSpace(config.Nats.CredsFile),
nkeySeedFile: strings.TrimSpace(config.Nats.NkeySeedFile),
jwtFile: strings.TrimSpace(config.Nats.JWTFile),
}
}

func resolveNatsAuthMode(authFiles natsAuthFiles) (natsAuthMode, error) {
if authFiles.credsFile != "" && (authFiles.nkeySeedFile != "" || authFiles.jwtFile != "") {
return natsAuthModeNone, errors.New("nats auth misconfiguration: nats.credsfile cannot be combined with nats.nkeyseedfile or nats.jwtfile")
}

if authFiles.jwtFile != "" && authFiles.nkeySeedFile == "" {
return natsAuthModeNone, errors.New("nats auth misconfiguration: nats.jwtfile requires nats.nkeyseedfile")
}

switch {
case authFiles.credsFile != "":
return natsAuthModeCredsFile, nil
case authFiles.jwtFile != "" && authFiles.nkeySeedFile != "":
return natsAuthModeJWTAndNkeySeedFile, nil
case authFiles.nkeySeedFile != "":
return natsAuthModeNkeySeedFile, nil
default:
return natsAuthModeNone, nil
}
}

func validateNatsAuthFile(path, configKey string) error {
_, err := os.Stat(path)
if err != nil {
return fmt.Errorf("nats auth misconfiguration: %s must reference a readable file: %w", configKey, err)
}

return nil
}

// ValidateNatsAuthConfig validates NATS auth modes and required file paths.
func ValidateNatsAuthConfig(config *types.Configuration) error {
authFiles := getNatsAuthFiles(config)
mode, err := resolveNatsAuthMode(authFiles)
if err != nil {
return err
}

switch mode {
case natsAuthModeCredsFile:
return validateNatsAuthFile(authFiles.credsFile, "nats.credsfile")
case natsAuthModeNkeySeedFile:
return validateNatsAuthFile(authFiles.nkeySeedFile, "nats.nkeyseedfile")
case natsAuthModeJWTAndNkeySeedFile:
if err = validateNatsAuthFile(authFiles.jwtFile, "nats.jwtfile"); err != nil {
return err
}
return validateNatsAuthFile(authFiles.nkeySeedFile, "nats.nkeyseedfile")
default:
return nil
}
}

func natsConnectOptions(config *types.Configuration) ([]nats.Option, error) {
authFiles := getNatsAuthFiles(config)
mode, err := resolveNatsAuthMode(authFiles)
if err != nil {
return nil, err
}

switch mode {
case natsAuthModeCredsFile:
return []nats.Option{nats.UserCredentials(authFiles.credsFile)}, nil
case natsAuthModeNkeySeedFile:
option, natsErr := nats.NkeyOptionFromSeed(authFiles.nkeySeedFile)
if natsErr != nil {
return nil, fmt.Errorf("nats auth misconfiguration: failed to load nats.nkeyseedfile: %w", natsErr)
}
return []nats.Option{option}, nil
case natsAuthModeJWTAndNkeySeedFile:
return []nats.Option{nats.UserCredentials(authFiles.jwtFile, authFiles.nkeySeedFile)}, nil
default:
return nil, nil
}
}

func natsTLSConnectOptions(config *types.Configuration, cfg types.CommonConfig) ([]nats.Option, error) {
if config == nil {
return nil, nil
}

hostPort := strings.TrimSpace(config.Nats.HostPort)
tlsRequested := cfg.MutualTLS || !cfg.CheckCert || config.TLSClient.CaCertFile != "" ||
strings.HasPrefix(hostPort, "tls://")
if !tlsRequested {
return nil, nil
}

pool, err := x509.SystemCertPool()
if err != nil {
pool = x509.NewCertPool()
}
tlsConfig := &tls.Config{
MinVersion: tls.VersionTLS12,
RootCAs: pool,
}

if config.TLSClient.CaCertFile != "" {
caCert, readErr := os.ReadFile(config.TLSClient.CaCertFile)
if readErr != nil {
return nil, fmt.Errorf("nats tls misconfiguration: failed to read tlsclient.cacertfile: %w", readErr)
}
tlsConfig.RootCAs.AppendCertsFromPEM(caCert)
}

if cfg.MutualTLS {
certPath := config.MutualTLSClient.CertFile
if certPath == "" {
certPath = config.MutualTLSFilesPath + MutualTLSClientCertFilename
}

keyPath := config.MutualTLSClient.KeyFile
if keyPath == "" {
keyPath = config.MutualTLSFilesPath + MutualTLSClientKeyFilename
}

caPath := config.MutualTLSClient.CaCertFile
if caPath == "" {
caPath = config.MutualTLSFilesPath + MutualTLSCacertFilename
}

cert, loadErr := tls.LoadX509KeyPair(certPath, keyPath)
if loadErr != nil {
return nil, fmt.Errorf("nats tls misconfiguration: failed to load mutualtlsclient cert/key: %w", loadErr)
}
tlsConfig.Certificates = []tls.Certificate{cert}

caCert, readErr := os.ReadFile(caPath)
if readErr != nil {
return nil, fmt.Errorf("nats tls misconfiguration: failed to read mutualtlsclient.cacertfile: %w", readErr)
}
tlsConfig.RootCAs.AppendCertsFromPEM(caCert)
} else if !cfg.CheckCert {
tlsConfig.InsecureSkipVerify = true // #nosec G402 This is only set as a result of explicit configuration
}

return []nats.Option{nats.Secure(tlsConfig)}, nil
}

// NatsPublish publishes event to NATS
func (c *Client) NatsPublish(falcopayload types.FalcoPayload) {
c.Stats.Nats.Add(Total, 1)
Expand All @@ -30,7 +201,23 @@ func (c *Client) NatsPublish(falcopayload types.FalcoPayload) {
subject = strings.ReplaceAll(subject, "<priority>", strings.ToLower(falcopayload.Priority.String()))
subject = strings.ReplaceAll(subject, "<rule>", strings.Trim(slugRegExp.ReplaceAllString(strings.ToLower(falcopayload.Rule), "_"), "_"))

nc, err := nats.Connect(c.EndpointURL.String())
options, err := natsConnectOptions(c.Config)
if err != nil {
c.setNatsErrorMetrics()
utils.Log(utils.ErrorLvl, c.OutputType, err.Error())
return
}

tlsOptions, err := natsTLSConnectOptions(c.Config, c.cfg)
if err != nil {
c.setNatsErrorMetrics()
utils.Log(utils.ErrorLvl, c.OutputType, err.Error())
return
}

options = append(options, tlsOptions...)

nc, err := nats.Connect(c.EndpointURL.String(), options...)
if err != nil {
c.setNatsErrorMetrics()
utils.Log(utils.ErrorLvl, c.OutputType, err.Error())
Expand Down
Loading