From 4d28eecc43ce7e1c70f1687b9ff59d19009131b5 Mon Sep 17 00:00:00 2001 From: Adam Chalkley Date: Tue, 10 Nov 2020 06:47:59 -0600 Subject: [PATCH] Add support for limiting payloads to specific IPs If provided, check remote IP Address against the list of user-provided IP Addresses. If a match is not found, reject the connection with forbidden status. If a match is found, accept the connection. If the sysadmin does not provide a list of trusted IP Addresses via config file or CLI flag, accept payloads from any IP Address. An attempt to balance this choice is provided by setting the default value in the starter config file to 127.0.0.1, or "localhost". This behavior is subject to change; the behavior may change to require an explicit list of IP Addresses to receive payloads. Update docs, config template to reflect the changes. refs GH-18 --- README.md | 11 +++++-- cmd/brick/handlers.go | 51 +++++++++++++++++++++++++++++++ cmd/brick/main.go | 10 ++++++ contrib/brick/config.example.toml | 6 ++++ doc.go | 2 ++ docs/configure.md | 3 ++ docs/deploy.md | 7 +++++ internal/config/getters.go | 21 +++++++++++++ internal/config/types.go | 6 ++++ internal/config/validate.go | 18 +++++++++++ 10 files changed, 132 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7a9b9283..cd062081 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,12 @@ See also: - Optional automatic (but not officially documented) termination of user sessions via official `ezproxy` binary +- Optional filtering of JSON payload sender IP Addresses + - default setting is to accept payloads from any IP Address, relying on + host-level firewall rules to prevent receipt from rouge systems + - if a list of trusted IP Addresses is provided, those IP Addresses will be + the only ones allowed to submit JSON payloads + - `es` CLI application - small CLI app to list and optionally terminate user sessions for a specific username @@ -154,6 +160,8 @@ See also: - Logging - Payload receipt from monitoring system + - logging of rejected payloads + - logging of accepted payloads - Action taken due to payload - username ignored - due to username inclusion in ignore file for usernames @@ -188,9 +196,6 @@ Known issues: - Documentation - The docs are beginning to take overall shape, but still need a lot of work -- Payloads are accepted from any IP Address - - the expectation is that host-level firewall rules will be used to protect - against this until a feature can be added to filter access (see GH-18) ### Future diff --git a/cmd/brick/handlers.go b/cmd/brick/handlers.go index 8c70b734..ee1bf14e 100644 --- a/cmd/brick/handlers.go +++ b/cmd/brick/handlers.go @@ -22,13 +22,16 @@ import ( "fmt" "io" "io/ioutil" + "net" "net/http" + "strings" "time" "github.com/apex/log" "github.com/atc0005/brick/internal/events" "github.com/atc0005/brick/internal/files" + "github.com/atc0005/brick/internal/textutils" ) // API endpoint patterns supported by this application @@ -87,6 +90,8 @@ func frontPageHandler(w http.ResponseWriter, r *http.Request) { } func disableUserHandler( + requireTrustedPayloadSender bool, + trustedPayloadSenders []string, reportedUserEventsLog *files.ReportedUserEventsLog, disabledUsers *files.DisabledUsers, ignoredSources files.IgnoredSources, @@ -103,6 +108,52 @@ func disableUserHandler( // fmt.Fprintf(mw, "disableUserHandler endpoint hit\n") log.Debug("disableUserHandler handler hit") + // If a list of trusted IPs is not provided by the sysadmin, the + // default behavior is to accept payloads from all IP Addresses. This + // behavior/logic is balanced by configuring the trusted IP Addresses + // list to include only 127.0.0.1 by default in the starter config + // file. + if requireTrustedPayloadSender { + // get bare sender IP + remoteIPAddr, _, ipHostSplitErr := net.SplitHostPort(events.GetIP(r)) + if ipHostSplitErr != nil { + errMsg := fmt.Sprintf( + "remote IP %q is not in IP:port format: %v", + remoteIPAddr, + ipHostSplitErr, + ) + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + fmt.Fprint(w, errMsg) + return + } + + // confirm that sender IP is in the trusted senders list + switch { + case !textutils.InList(remoteIPAddr, trustedPayloadSenders): + errMsg := fmt.Sprintf( + "rejecting payload; remote IP %q is not in the trusted payload senders list: %v", + remoteIPAddr, + trustedPayloadSenders, + ) + log.WithFields(log.Fields{ + "url_path": r.URL.Path, + "http_method": r.Method, + "remote_ip_addr": remoteIPAddr, + "trusted_payload_senders": strings.Join(trustedPayloadSenders, ", "), + }).Error(errMsg) + http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden) + fmt.Fprint(w, errMsg) + return + + default: + log.Infof( + "payload accepted; remote IP %q is in the trusted payload senders list: %v", + remoteIPAddr, + trustedPayloadSenders, + ) + } + } + if r.Method != http.MethodPost { log.WithFields(log.Fields{ diff --git a/cmd/brick/main.go b/cmd/brick/main.go index 73a999b1..c7593e75 100644 --- a/cmd/brick/main.go +++ b/cmd/brick/main.go @@ -150,6 +150,14 @@ func main() { appConfig.IgnoreLookupErrors(), ) + // log this to help troubleshoot why payloads are (or are not) filtered + switch { + case appConfig.RequireTrustedPayloadSender(): + log.Info("OK: Restricting payload sender IP Addresses enabled") + default: + log.Warn("CAUTION: Restricting payload sender IP Addresses disabled") + } + // GET requests mux.HandleFunc(frontpageEndpointPattern, frontPageHandler) mux.HandleFunc(apiV1ViewDisabledUsersEndpointPattern, viewDisabledUsersHandler) @@ -159,6 +167,8 @@ func main() { mux.HandleFunc( apiV1DisableUserEndpointPattern, disableUserHandler( + appConfig.RequireTrustedPayloadSender(), + appConfig.TrustedIPAddresses(), reportedUserEventsLog, disabledUsers, ignoredSources, diff --git a/contrib/brick/config.example.toml b/contrib/brick/config.example.toml index 3c630827..54a1ccd1 100644 --- a/contrib/brick/config.example.toml +++ b/contrib/brick/config.example.toml @@ -30,6 +30,12 @@ local_tcp_port = 8000 # Local IP Address that this application should listen on for incoming HTTP requests. local_ip_address = "localhost" +# One or many single IP Addresses which are trusted for payload submission. If +# this is defined, all other sender IPs are ignored. If this is not defined, +# payloads are accepted from all IP Addresses not otherwise rejected by +# local/remote firewall rules. +trusted_ip_addresses = ["127.0.0.1"] + [logging] diff --git a/doc.go b/doc.go index daf94804..6dfa1533 100644 --- a/doc.go +++ b/doc.go @@ -62,6 +62,8 @@ FEATURES • Optional automatic (but not officially documented) termination of user sessions via official EZproxy binary +• Optional filtering of JSON payload sender IP Addresses + USAGE See the README for examples. diff --git a/docs/configure.md b/docs/configure.md index 337c4b70..ce770165 100644 --- a/docs/configure.md +++ b/docs/configure.md @@ -40,6 +40,7 @@ environment variables) and use the configuration file for the other settings. | `ignore-lookup-errors` | No | `false` | No | `true`, `false` | Whether application should continue if attempts to lookup existing disabled or ignored status for a username or IP Address fail. This is needed if you do not pre-create files used by this application ahead of time. WARNING: Because this can mask errors, you should probably only use it briefly when this application is first deployed, then later disable the setting once all files are in place. | | `port` | No | `8000` | No | *valid TCP port number* | TCP port that this application should listen on for incoming HTTP requests. Tip: Use an unreserved port between 1024:49151 (inclusive) for the best results. | | `ip-address` | No | `localhost` | No | *valid fqdn, local name or IP Address* | Local IP Address that this application should listen on for incoming HTTP requests. | +| `trusted-ip-addresses` | No | **all** | No | *one or many valid fqdn or IP Addresses* | One or many single IP Addresses which are trusted for payload submission. If this is defined, all other sender IPs are ignored. If this is not defined, payloads are accepted from all IP Addresses not otherwise rejected by local/remote firewall rules. | | `log-level` | No | `info` | No | `fatal`, `error`, `warn`, `info`, `debug` | Log message priority filter. Log messages with a lower level are ignored. | | `log-output` | No | `stdout` | No | `stdout`, `stderr` | Log messages are written to this output target. | | `log-format` | No | `text` | No | `cli`, `json`, `logfmt`, `text`, `discard` | Use the specified `apex/log` package "handler" to output log messages in that handler's format. | @@ -82,6 +83,7 @@ Arguments](#command-line-arguments) table for more information. | `ignore-lookup-errors` | `BRICK_IGNORE_LOOKUP_ERRORS` | | `BRICK_IGNORE_LOOKUP_ERRORS="false"` | | `port` | `BRICK_LOCAL_TCP_PORT` | | `BRICK_LOCAL_TCP_PORT="8000"` | | `ip-address` | `BRICK_LOCAL_IP_ADDRESS` | | `BRICK_LOCAL_IP_ADDRESS="localhost"` | +| `trusted-ip-addresses` | `BRICK_TRUSTED_IP_ADDRESSES` | | `BRICK_TRUSTED_IP_ADDRESSES="127.0.0.1"` | | `log-level` | `BRICK_LOG_LEVEL` | | `BRICK_LOG_LEVEL="info"` | | `log-output` | `BRICK_LOG_OUTPUT` | | `BRICK_LOG_OUTPUT="stdout"` | | `log-format` | `BRICK_LOG_FORMAT` | | `BRICK_LOG_FORMAT="text"` | @@ -124,6 +126,7 @@ settings. | `ignore-lookup-errors` | `ignore_lookup_errors` | | | | `port` | `local_tcp_port` | `network` | | | `ip-address` | `local_ip_address` | `network` | | +| `trusted-ip-addresses` | `trusted_ip_addresses` | `network` | [Multi-line array](https://github.com/toml-lang/toml#user-content-array) | | `log-level` | `level` | `logging` | | | `log-format` | `format` | `logging` | | | `log-output` | `output` | `logging` | | diff --git a/docs/deploy.md b/docs/deploy.md index 8ad812f9..c4f1c2d2 100644 --- a/docs/deploy.md +++ b/docs/deploy.md @@ -177,6 +177,13 @@ version. 1. Copy the starter/template configuration file from `contrib/brick/config.example.toml` and modify accordingly using the [configuration](configure.md) guide. +1. Configure one or more trusted IP Addresses + - Per the default setting in the config file, only payload submissions from + `127.0.0.1` will be accepted. You need to enter the IP Address of the + Splunk system which is responsible for delivering JSON payloads to this + application. + - **CAUTION**: If you do not define a value for this setting, or comment it + out, payload submissions from all sender IP Addresses will be accepted. 1. Decide whether you will enable automatic sessions termination or use `fail2ban`. See the [fail2ban](fail2ban.md) doc and the [configuration](configure.md) guide for more information. diff --git a/internal/config/getters.go b/internal/config/getters.go index 0f18a6ed..d9689a5f 100644 --- a/internal/config/getters.go +++ b/internal/config/getters.go @@ -97,6 +97,27 @@ func (c Config) LocalIPAddress() string { } } +// RequireTrustedPayloadSender indicates whether the sysadmin specified a list +// of IP Addresses to trust for payload submission. +func (c Config) RequireTrustedPayloadSender() bool { + return c.cliConfig.Network.TrustedIPAddresses != nil || + c.fileConfig.Network.TrustedIPAddresses != nil +} + +// TrustedIPAddresses returns the user-provided list of IP Addresses that +// should be trusted to receive payloads or the the default value if not +// provided. CLI flag values take precedence if provided. +func (c Config) TrustedIPAddresses() []string { + switch { + case c.cliConfig.Network.TrustedIPAddresses != nil: + return c.cliConfig.Network.TrustedIPAddresses + case c.fileConfig.Network.TrustedIPAddresses != nil: + return c.fileConfig.Network.TrustedIPAddresses + default: + return []string{} + } +} + // DisabledUsersFile returns the user-provided path to the EZproxy include // file where this application should write disabled user accounts or the // default value if not provided. CLI flag values take precedence if provided. diff --git a/internal/config/types.go b/internal/config/types.go index d6d689af..e0ad20e8 100644 --- a/internal/config/types.go +++ b/internal/config/types.go @@ -48,6 +48,12 @@ type Network struct { // LocalIPAddress is the IP Address that this application should listen on // for incoming requests LocalIPAddress *string `toml:"local_ip_address" arg:"--ip-address,env:BRICK_LOCAL_IP_ADDRESS" help:"Local IP Address that this application should listen on for incoming HTTP requests."` + + // TrustedIPAddresses is the collection of single IP Addresses which are + // trusted for payload submission. If this is defined, all other sender + // IPs are ignored. If this is not defined, payloads are accepted from all + // IP Addresses not otherwise rejected by local/remote firewall rules. + TrustedIPAddresses []string `toml:"trusted_ip_addresses" arg:"--trusted-ip-addresses,env:BRICK_TRUSTED_IP_ADDRESSES" help:"One or many single IP Addresses which are trusted for payload submission. If this is defined, all other sender IPs are ignored. If this is not defined, payloads are accepted from all IP Addresses not otherwise rejected by local/remote firewall rules."` } // Logging is a collection of logging-related settings provided via CLI and diff --git a/internal/config/validate.go b/internal/config/validate.go index 016f0295..cc962449 100644 --- a/internal/config/validate.go +++ b/internal/config/validate.go @@ -18,6 +18,7 @@ package config import ( "fmt" + "net" "github.com/apex/log" @@ -142,6 +143,23 @@ func validate(c Config) error { return fmt.Errorf("local IP Address not provided") } + // true if sysadmin specified a value via CLI or config file + if c.RequireTrustedPayloadSender() { + switch { + case len(c.TrustedIPAddresses()) < 1: + return fmt.Errorf("empty list of trusted IP Addresses provided") + default: + for _, ipAddr := range c.TrustedIPAddresses() { + if net.ParseIP(ipAddr) == nil { + return fmt.Errorf( + "invalid IP Address %q provided for trusted IPs list", + ipAddr, + ) + } + } + } + } + switch c.LogLevel() { case LogLevelFatal: case LogLevelError: