Add HTTP basic auth support to the http_listener input (#3496)
This commit is contained in:
parent
5389f40057
commit
798fea3109
|
@ -2862,6 +2862,11 @@
|
||||||
# ## Add service certificate and key
|
# ## Add service certificate and key
|
||||||
# tls_cert = "/etc/telegraf/cert.pem"
|
# tls_cert = "/etc/telegraf/cert.pem"
|
||||||
# tls_key = "/etc/telegraf/key.pem"
|
# tls_key = "/etc/telegraf/key.pem"
|
||||||
|
#
|
||||||
|
# ## Optional username and password to accept for HTTP basic authentication.
|
||||||
|
# ## You probably want to make sure you have TLS configured above for this.
|
||||||
|
# basic_username = "foobar"
|
||||||
|
# basic_password = "barfoo"
|
||||||
|
|
||||||
|
|
||||||
# # Read metrics from Kafka topic(s)
|
# # Read metrics from Kafka topic(s)
|
||||||
|
|
|
@ -12,6 +12,8 @@ Enable TLS by specifying the file names of a service TLS certificate and key.
|
||||||
|
|
||||||
Enable mutually authenticated TLS and authorize client connections by signing certificate authority by including a list of allowed CA certificate file names in ````tls_allowed_cacerts````.
|
Enable mutually authenticated TLS and authorize client connections by signing certificate authority by including a list of allowed CA certificate file names in ````tls_allowed_cacerts````.
|
||||||
|
|
||||||
|
Enable basic HTTP authentication of clients by specifying a username and password to check for. These credentials will be received from the client _as plain text_ if TLS is not configured.
|
||||||
|
|
||||||
See: [Telegraf Input Data Formats](https://github.com/influxdata/telegraf/blob/master/docs/DATA_FORMATS_INPUT.md#influx).
|
See: [Telegraf Input Data Formats](https://github.com/influxdata/telegraf/blob/master/docs/DATA_FORMATS_INPUT.md#influx).
|
||||||
|
|
||||||
**Example:**
|
**Example:**
|
||||||
|
@ -39,4 +41,8 @@ This is a sample configuration for the plugin.
|
||||||
|
|
||||||
## MTLS
|
## MTLS
|
||||||
tls_allowed_cacerts = ["/etc/telegraf/clientca.pem"]
|
tls_allowed_cacerts = ["/etc/telegraf/clientca.pem"]
|
||||||
|
|
||||||
|
## Basic authentication
|
||||||
|
basic_username = "foobar"
|
||||||
|
basic_password = "barfoo"
|
||||||
```
|
```
|
||||||
|
|
|
@ -3,6 +3,7 @@ package http_listener
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"compress/gzip"
|
"compress/gzip"
|
||||||
|
"crypto/subtle"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"io"
|
"io"
|
||||||
|
@ -44,6 +45,9 @@ type HTTPListener struct {
|
||||||
TlsCert string
|
TlsCert string
|
||||||
TlsKey string
|
TlsKey string
|
||||||
|
|
||||||
|
BasicUsername string
|
||||||
|
BasicPassword string
|
||||||
|
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
|
|
||||||
|
@ -64,6 +68,7 @@ type HTTPListener struct {
|
||||||
PingsRecv selfstat.Stat
|
PingsRecv selfstat.Stat
|
||||||
NotFoundsServed selfstat.Stat
|
NotFoundsServed selfstat.Stat
|
||||||
BuffersCreated selfstat.Stat
|
BuffersCreated selfstat.Stat
|
||||||
|
AuthFailures selfstat.Stat
|
||||||
}
|
}
|
||||||
|
|
||||||
const sampleConfig = `
|
const sampleConfig = `
|
||||||
|
@ -90,6 +95,11 @@ const sampleConfig = `
|
||||||
## Add service certificate and key
|
## Add service certificate and key
|
||||||
tls_cert = "/etc/telegraf/cert.pem"
|
tls_cert = "/etc/telegraf/cert.pem"
|
||||||
tls_key = "/etc/telegraf/key.pem"
|
tls_key = "/etc/telegraf/key.pem"
|
||||||
|
|
||||||
|
## Optional username and password to accept for HTTP basic authentication.
|
||||||
|
## You probably want to make sure you have TLS configured above for this.
|
||||||
|
# basic_username = "foobar"
|
||||||
|
# basic_password = "barfoo"
|
||||||
`
|
`
|
||||||
|
|
||||||
func (h *HTTPListener) SampleConfig() string {
|
func (h *HTTPListener) SampleConfig() string {
|
||||||
|
@ -124,6 +134,7 @@ func (h *HTTPListener) Start(acc telegraf.Accumulator) error {
|
||||||
h.PingsRecv = selfstat.Register("http_listener", "pings_received", tags)
|
h.PingsRecv = selfstat.Register("http_listener", "pings_received", tags)
|
||||||
h.NotFoundsServed = selfstat.Register("http_listener", "not_founds_served", tags)
|
h.NotFoundsServed = selfstat.Register("http_listener", "not_founds_served", tags)
|
||||||
h.BuffersCreated = selfstat.Register("http_listener", "buffers_created", tags)
|
h.BuffersCreated = selfstat.Register("http_listener", "buffers_created", tags)
|
||||||
|
h.AuthFailures = selfstat.Register("http_listener", "auth_failures", tags)
|
||||||
|
|
||||||
if h.MaxBodySize == 0 {
|
if h.MaxBodySize == 0 {
|
||||||
h.MaxBodySize = DEFAULT_MAX_BODY_SIZE
|
h.MaxBodySize = DEFAULT_MAX_BODY_SIZE
|
||||||
|
@ -194,25 +205,29 @@ func (h *HTTPListener) ServeHTTP(res http.ResponseWriter, req *http.Request) {
|
||||||
case "/write":
|
case "/write":
|
||||||
h.WritesRecv.Incr(1)
|
h.WritesRecv.Incr(1)
|
||||||
defer h.WritesServed.Incr(1)
|
defer h.WritesServed.Incr(1)
|
||||||
h.serveWrite(res, req)
|
h.AuthenticateIfSet(h.serveWrite, res, req)
|
||||||
case "/query":
|
case "/query":
|
||||||
h.QueriesRecv.Incr(1)
|
h.QueriesRecv.Incr(1)
|
||||||
defer h.QueriesServed.Incr(1)
|
defer h.QueriesServed.Incr(1)
|
||||||
// Deliver a dummy response to the query endpoint, as some InfluxDB
|
// Deliver a dummy response to the query endpoint, as some InfluxDB
|
||||||
// clients test endpoint availability with a query
|
// clients test endpoint availability with a query
|
||||||
|
h.AuthenticateIfSet(func(res http.ResponseWriter, req *http.Request) {
|
||||||
res.Header().Set("Content-Type", "application/json")
|
res.Header().Set("Content-Type", "application/json")
|
||||||
res.Header().Set("X-Influxdb-Version", "1.0")
|
res.Header().Set("X-Influxdb-Version", "1.0")
|
||||||
res.WriteHeader(http.StatusOK)
|
res.WriteHeader(http.StatusOK)
|
||||||
res.Write([]byte("{\"results\":[]}"))
|
res.Write([]byte("{\"results\":[]}"))
|
||||||
|
}, res, req)
|
||||||
case "/ping":
|
case "/ping":
|
||||||
h.PingsRecv.Incr(1)
|
h.PingsRecv.Incr(1)
|
||||||
defer h.PingsServed.Incr(1)
|
defer h.PingsServed.Incr(1)
|
||||||
// respond to ping requests
|
// respond to ping requests
|
||||||
|
h.AuthenticateIfSet(func(res http.ResponseWriter, req *http.Request) {
|
||||||
res.WriteHeader(http.StatusNoContent)
|
res.WriteHeader(http.StatusNoContent)
|
||||||
|
}, res, req)
|
||||||
default:
|
default:
|
||||||
defer h.NotFoundsServed.Incr(1)
|
defer h.NotFoundsServed.Incr(1)
|
||||||
// Don't know how to respond to calls to other endpoints
|
// Don't know how to respond to calls to other endpoints
|
||||||
http.NotFound(res, req)
|
h.AuthenticateIfSet(http.NotFound, res, req)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -376,6 +391,23 @@ func (h *HTTPListener) getTLSConfig() *tls.Config {
|
||||||
return tlsConf
|
return tlsConf
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *HTTPListener) AuthenticateIfSet(handler http.HandlerFunc, res http.ResponseWriter, req *http.Request) {
|
||||||
|
if h.BasicUsername != "" && h.BasicPassword != "" {
|
||||||
|
reqUsername, reqPassword, ok := req.BasicAuth()
|
||||||
|
if !ok ||
|
||||||
|
subtle.ConstantTimeCompare([]byte(reqUsername), []byte(h.BasicUsername)) != 1 ||
|
||||||
|
subtle.ConstantTimeCompare([]byte(reqPassword), []byte(h.BasicPassword)) != 1 {
|
||||||
|
|
||||||
|
h.AuthFailures.Incr(1)
|
||||||
|
http.Error(res, "Unauthorized.", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
handler(res, req)
|
||||||
|
} else {
|
||||||
|
handler(res, req)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
inputs.Add("http_listener", func() telegraf.Input {
|
inputs.Add("http_listener", func() telegraf.Input {
|
||||||
return &HTTPListener{
|
return &HTTPListener{
|
||||||
|
|
|
@ -101,6 +101,9 @@ NsFlcGACj+/TvacFYlA6N2nyFeokzoqLX28Ddxdh2erXqJ4hYIhT1ik9tkLggs2z
|
||||||
1T1084BquCuO6lIcOwJBALX4xChoMUF9k0IxSQzlz//seQYDkQNsE7y9IgAOXkzp
|
1T1084BquCuO6lIcOwJBALX4xChoMUF9k0IxSQzlz//seQYDkQNsE7y9IgAOXkzp
|
||||||
RaR4pzgPbnKj7atG+2dBnffWfE+1Mcy0INDAO6WxPg0=
|
RaR4pzgPbnKj7atG+2dBnffWfE+1Mcy0INDAO6WxPg0=
|
||||||
-----END RSA PRIVATE KEY-----`
|
-----END RSA PRIVATE KEY-----`
|
||||||
|
|
||||||
|
basicUsername = "test-username-please-ignore"
|
||||||
|
basicPassword = "super-secure-password!"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -120,6 +123,13 @@ func newTestHTTPListener() *HTTPListener {
|
||||||
return listener
|
return listener
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newTestHTTPAuthListener() *HTTPListener {
|
||||||
|
listener := newTestHTTPListener()
|
||||||
|
listener.BasicUsername = basicUsername
|
||||||
|
listener.BasicPassword = basicPassword
|
||||||
|
return listener
|
||||||
|
}
|
||||||
|
|
||||||
func newTestHTTPSListener() *HTTPListener {
|
func newTestHTTPSListener() *HTTPListener {
|
||||||
initServiceCertFiles.Do(func() {
|
initServiceCertFiles.Do(func() {
|
||||||
acaf, err := ioutil.TempFile("", "allowedCAFile.crt")
|
acaf, err := ioutil.TempFile("", "allowedCAFile.crt")
|
||||||
|
@ -239,6 +249,24 @@ func TestWriteHTTPSWithClientAuth(t *testing.T) {
|
||||||
require.EqualValues(t, 204, resp.StatusCode)
|
require.EqualValues(t, 204, resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestWriteHTTPBasicAuth(t *testing.T) {
|
||||||
|
listener := newTestHTTPAuthListener()
|
||||||
|
|
||||||
|
acc := &testutil.Accumulator{}
|
||||||
|
require.NoError(t, listener.Start(acc))
|
||||||
|
defer listener.Stop()
|
||||||
|
|
||||||
|
client := &http.Client{}
|
||||||
|
|
||||||
|
req, err := http.NewRequest("POST", createURL(listener, "http", "/write", "db=mydb"), bytes.NewBuffer([]byte(testMsg)))
|
||||||
|
require.NoError(t, err)
|
||||||
|
req.SetBasicAuth(basicUsername, basicPassword)
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
resp.Body.Close()
|
||||||
|
require.EqualValues(t, http.StatusNoContent, resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
func TestWriteHTTP(t *testing.T) {
|
func TestWriteHTTP(t *testing.T) {
|
||||||
listener := newTestHTTPListener()
|
listener := newTestHTTPListener()
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue