Skip to content

Commit

Permalink
[Filebeat][http_endpoint input] Adds support for custom auth header n…
Browse files Browse the repository at this point in the history
…ames and secret (elastic#20435)
  • Loading branch information
P1llus committed Aug 10, 2020
1 parent 8593c85 commit d4fb2bc
Show file tree
Hide file tree
Showing 6 changed files with 106 additions and 6 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.next.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,7 @@ https://github.com/elastic/beats/compare/v7.0.0-alpha2...master[Check the HEAD d
- Add event.ingested for CrowdStrike module {pull}20138[20138]
- Add support for additional fields and FirewallMatchEvent type events in CrowdStrike module {pull}20138[20138]
- Add event.ingested for Suricata module {pull}20220[20220]
- Add support for custom header and headersecret for filebeat http_endpoint input {pull}20435[20435]
- Add event.ingested to all Filebeat modules. {pull}20386[20386]
- Return error when log harvester tries to open a named pipe. {issue}18682[18682] {pull}20450[20450]

Expand Down
22 changes: 22 additions & 0 deletions x-pack/filebeat/docs/inputs/input-http-endpoint.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,18 @@ Basic auth and SSL example:
password: somepassword
----

Authentication or checking that a specific header includes a specific value
["source","yaml",subs="attributes"]
----
{beatname_lc}.inputs:
- type: http_endpoint
enabled: true
listen_address: 192.168.1.1
listen_port: 8080
secret.header: someheadername
secret.value: secretheadertoken
----


==== Configuration options

Expand All @@ -91,6 +103,16 @@ If `basic_auth` is enabled, this is the username used for authentication against

If `basic_auth` is eanbled, this is the password used for authentication against the HTTP listener. Requires `username` to also be set.

[float]
==== `secret.header`

The header to check for a specific value specified by `secret.value`. Certain webhooks provide the possibility to include a special header and secret to identify the source.

[float]
==== `secret.value`

The secret stored in the header name specified by `secret.header`. Certain webhooks provide the possibility to include a special header and secret to identify the source.

[float]
==== `content_type`

Expand Down
8 changes: 8 additions & 0 deletions x-pack/filebeat/input/http_endpoint/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ type config struct {
URL string `config:"url"`
Prefix string `config:"prefix"`
ContentType string `config:"content_type"`
SecretHeader string `config:"secret.header"`
SecretValue string `config:"secret.value"`
}

func defaultConfig() config {
Expand All @@ -38,6 +40,8 @@ func defaultConfig() config {
URL: "/",
Prefix: "json",
ContentType: "application/json",
SecretHeader: "",
SecretValue: "",
}
}

Expand All @@ -52,5 +56,9 @@ func (c *config) Validate() error {
}
}

if (c.SecretHeader != "" && c.SecretValue == "") || (c.SecretHeader == "" && c.SecretValue != "") {
return errors.New("Both secret.header and secret.value must be set")
}

return nil
}
12 changes: 7 additions & 5 deletions x-pack/filebeat/input/http_endpoint/input.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,13 @@ func (e *httpEndpoint) Run(ctx v2.Context, publisher stateless.Publisher) error
log := ctx.Logger.With("address", e.addr)

validator := &apiValidator{
basicAuth: e.config.BasicAuth,
username: e.config.Username,
password: e.config.Password,
method: http.MethodPost,
contentType: e.config.ContentType,
basicAuth: e.config.BasicAuth,
username: e.config.Username,
password: e.config.Password,
method: http.MethodPost,
contentType: e.config.ContentType,
secretHeader: e.config.SecretHeader,
secretValue: e.config.SecretValue,
}

handler := &httpHandler{
Expand Down
9 changes: 9 additions & 0 deletions x-pack/filebeat/input/http_endpoint/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,12 @@ type apiValidator struct {
username, password string
method string
contentType string
secretHeader string
secretValue string
}

var errIncorrectUserOrPass = errors.New("Incorrect username or password")
var errIncorrectHeaderSecret = errors.New("Incorrect header or header secret")

func (v *apiValidator) ValidateHeader(r *http.Request) (int, error) {
if v.basicAuth {
Expand All @@ -33,6 +36,12 @@ func (v *apiValidator) ValidateHeader(r *http.Request) (int, error) {
}
}

if v.secretHeader != "" && v.secretValue != "" {
if v.secretValue != r.Header.Get(v.secretHeader) {
return http.StatusUnauthorized, errIncorrectHeaderSecret
}
}

if v.method != "" && v.method != r.Method {
return http.StatusMethodNotAllowed, fmt.Errorf("Only %v requests supported", v.method)
}
Expand Down
60 changes: 59 additions & 1 deletion x-pack/filebeat/tests/system/test_http_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ def test_http_endpoint_request(self):

output = self.read_output()

print("response:", r.status_code, r.text)

assert r.text == '{"message": "success"}'
assert output[0]["input.type"] == "http_endpoint"
assert output[0]["json.{}".format(self.prefix)] == message
Expand All @@ -98,6 +100,8 @@ def test_http_endpoint_wrong_content_header(self):

filebeat.check_kill_and_wait()

print("response:", r.status_code, r.text)

assert r.status_code == 415
assert r.text == '{"message": "Wrong Content-Type header, expecting application/json"}'

Expand Down Expand Up @@ -135,9 +139,59 @@ def test_http_endpoint_wrong_auth_value(self):

filebeat.check_kill_and_wait()

print("response:", r.status_code, r.text)

assert r.status_code == 401
assert r.text == '{"message": "Incorrect username or password"}'

def test_http_endpoint_wrong_auth_header(self):
"""
Test http_endpoint input with wrong auth header and secret.
"""
options = """
secret.header: Authorization
secret.value: 123password
"""
self.get_config(options)
filebeat = self.start_beat()
self.wait_until(lambda: self.log_contains("Starting HTTP server on {}:{}".format(self.host, self.port)))

message = "somerandommessage"
payload = {self.prefix: message}
headers = {"Content-Type": "application/json", "Authorization": "password123"}
r = requests.post(self.url, headers=headers, data=json.dumps(payload))

filebeat.check_kill_and_wait()

print("response:", r.status_code, r.text)

assert r.status_code == 401
assert r.text == '{"message": "Incorrect header or header secret"}'

def test_http_endpoint_correct_auth_header(self):
"""
Test http_endpoint input with correct auth header and secret.
"""
options = """
secret.header: Authorization
secret.value: 123password
"""
self.get_config(options)
filebeat = self.start_beat()
self.wait_until(lambda: self.log_contains("Starting HTTP server on {}:{}".format(self.host, self.port)))

message = "somerandommessage"
payload = {self.prefix: message}
headers = {"Content-Type": "application/json", "Authorization": "123password"}
r = requests.post(self.url, headers=headers, data=json.dumps(payload))

filebeat.check_kill_and_wait()
output = self.read_output()

assert r.text == '{"message": "success"}'
assert output[0]["input.type"] == "http_endpoint"
assert output[0]["json.{}".format(self.prefix)] == message

def test_http_endpoint_empty_body(self):
"""
Test http_endpoint input with empty body.
Expand All @@ -151,6 +205,8 @@ def test_http_endpoint_empty_body(self):

filebeat.check_kill_and_wait()

print("response:", r.status_code, r.text)

assert r.status_code == 406
assert r.text == '{"message": "Body cannot be empty"}'

Expand All @@ -169,6 +225,7 @@ def test_http_endpoint_malformed_json(self):
filebeat.check_kill_and_wait()

print("response:", r.status_code, r.text)

assert r.status_code == 400
assert r.text.startswith('{"message": "Malformed JSON body:')

Expand All @@ -184,8 +241,9 @@ def test_http_endpoint_get_request(self):
payload = {self.prefix: message}
headers = {"Content-Type": "application/json", "Accept": "application/json"}
r = requests.get(self.url, headers=headers, data=json.dumps(payload))
print("response:", r.status_code, r.text)
filebeat.check_kill_and_wait()

print("response:", r.status_code, r.text)

assert r.status_code == 405
assert r.text == '{"message": "Only POST requests supported"}'

0 comments on commit d4fb2bc

Please sign in to comment.