Skip to content
This repository was archived by the owner on Mar 22, 2018. It is now read-only.

Commit 476057f

Browse files
author
Johannes Meyer
committed
Don't blindly trust header values without being instructed to do so
We were vulnerable to various spoofing attacks prior to this change..
1 parent cd47cad commit 476057f

6 files changed

Lines changed: 74 additions & 24 deletions

File tree

README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,11 @@ See [here](doc/03-Configuration.md#configuration) for how to configure ElasticAr
2727

2828
## Authentication
2929

30-
By default ElasticArmor considers any request with basic authentication credentials as being authenticated. This
31-
allows another reverse proxy being placed in front of ElasticArmor that is performing the actual authentication.
32-
But it is also possible to have ElasticArmor performing the authentication by configuring one or more
33-
authentication mechanisms.
30+
While ElasticArmor can *run* without any configuration, it does not authenticate clients without further ado. If
31+
you have another reverse proxy placed in front of ElasticArmor that is performing the actual authentication, you
32+
need to define it as [trusted proxy](doc/03-Configuration.md#configuration-proxy-trusted-proxies) in order to make
33+
ElasticArmor blindly trust a request's credentials. If you want ElasticArmor to perform the authentication, it
34+
is possible to configure one or more mechanisms to accomplish this.
3435

3536
See [here](doc/04-Authentication.md#authentication) for how to configure authentication.
3637

doc/01-About.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,11 @@ See [here](03-Configuration.md#configuration) for how to configure ElasticArmor.
2727

2828
## <a id="about-authentication"></a> Authentication
2929

30-
By default ElasticArmor considers any request with basic authentication credentials as being authenticated. This
31-
allows another reverse proxy being placed in front of ElasticArmor that is performing the actual authentication.
32-
But it is also possible to have ElasticArmor performing the authentication by configuring one or more
33-
authentication mechanisms.
30+
While ElasticArmor can *run* without any configuration, it does not authenticate clients without further ado. If
31+
you have another reverse proxy placed in front of ElasticArmor that is performing the actual authentication, you
32+
need to define it as [trusted proxy](03-Configuration.md#configuration-proxy-trusted-proxies) in order to make
33+
ElasticArmor blindly trust a request's credentials. If you want ElasticArmor to perform the authentication, it
34+
is possible to configure one or more mechanisms to accomplish this.
3435

3536
See [here](04-Authentication.md#authentication) for how to configure authentication.
3637

doc/03-Configuration.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,38 @@ You can set a list of host[:port] combinations separated by comma on the option
5555
...
5656
allow_from="localhost, name.example.com, 10.20.30.40:1234"
5757

58+
### <a id="configuration-proxy-trusted-proxies"></a> Trusted Proxies
59+
60+
Proxies configured as being trusted allow ElasticArmor to utilize a request's HTTP headers for authorization.
61+
62+
You can set a list of host[:port] combinations separated by comma on the option *trusted_proxies*:
63+
64+
[proxy]
65+
...
66+
trusted_proxies="localhost, name.example.com, 10.20.30.40:1234"
67+
68+
The HTTP headers utilized by ElasticArmor for authorization are as follows:
69+
70+
#### <a id="configuration-proxy-trusted-proxies-authorization"></a> Authorization
71+
72+
In case ElasticArmor does not authenticate clients on its own, the *Authorization* header is only used for
73+
identification purposes. This means that requests containing authentication credentials received from a
74+
trusted proxy are considered as being successfully authenticated.
75+
76+
> **Note:**
77+
>
78+
> In case ElasticArmor authenticates clients on its own, they still need to authenticate with the configured
79+
> mechanisms regardless from whom the request has been received.
80+
81+
#### <a id="configuration-proxy-trusted-proxies-x-forwarded-for"></a> X-Forwarded-For
82+
83+
The XFF header is used to replace a request's origin address and port with the very first entry supplied with
84+
this header.
85+
86+
> **Note:**
87+
>
88+
> If you have anonymous access configured, the extracted entry is subject to authorization instead.
89+
5890
### <a id="configuration-proxy-fallback-nodes"></a> Fallback Nodes
5991

6092
It is possible to define multiple Elasticsearch nodes, each separated by comma:

lib/elasticarmor/auth/__init__.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ def __init__(self, settings):
2626
self.role_backend = settings.role_backend
2727
self.auth_backends = settings.auth_backends
2828
self.group_backends = settings.group_backends
29+
self.trusted_proxies = settings.trusted_proxies
2930

3031
def authenticate(self, client, populate=True):
3132
"""Authenticate the given client and return whether it succeeded or not."""
@@ -60,7 +61,8 @@ def authenticate(self, client, populate=True):
6061
self.log.error('Failed to authenticate client "%s" using backend "%s". %s.',
6162
client, backend.name, format_ldap_error(error))
6263
else:
63-
client.authenticated = True
64+
trusted_ports = self.trusted_proxies.get(client.peer_address, [])
65+
client.authenticated = trusted_ports is None or client.peer_port in trusted_ports
6466

6567
if client.authenticated and populate:
6668
self.populate(client)
@@ -171,8 +173,12 @@ def __init__(self, address, port=None):
171173
self.address = address
172174
self.port = port
173175

176+
self.peer_address = None
177+
self.peer_port = None
178+
174179
self.name = None
175180
self.authenticated = False
181+
self.default_role = None
176182
self.username = None
177183
self.password = None
178184
self.groups = None

lib/elasticarmor/proxy.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -259,13 +259,16 @@ def client(self):
259259
if self._client is not None:
260260
return self._client
261261

262-
client_address = client_port = None
262+
client_address, client_port = self.client_address
263263
if self._context is not None:
264-
client_address, client_port = self._context.forwarded_for
265-
if client_address is None:
266-
client_address, client_port = self.client_address
264+
trusted_ports = self.server.auth.trusted_proxies.get(client_address, [])
265+
if trusted_ports is None or client_port in trusted_ports:
266+
forwarded_address, forwarded_port = self._context.forwarded_for
267+
if forwarded_address is not None:
268+
client_address, client_port = forwarded_address, forwarded_port
267269

268270
self._client = Client(client_address, client_port)
271+
self._client.peer_address, self._client.peer_port = self.client_address
269272

270273
try:
271274
header_value = self.headers['Authorization']

lib/elasticarmor/settings.py

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -246,14 +246,21 @@ def certificate(self):
246246

247247
@property
248248
def allow_from(self):
249-
allow_from = {}
249+
try:
250+
return self._create_network_map(self.config.get('proxy', 'allow_from'))
251+
except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
252+
return {}
250253

254+
@property
255+
def trusted_proxies(self):
251256
try:
252-
value = self.config.get('proxy', 'allow_from')
257+
return self._create_network_map(self.config.get('proxy', 'trusted_proxies'))
253258
except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
254-
return allow_from
259+
return {}
255260

256-
for host_and_port in value.split(','):
261+
def _create_network_map(self, hosts):
262+
network_map = {}
263+
for host_and_port in hosts.split(','):
257264
try:
258265
host, port = host_and_port.split(':')
259266
except ValueError:
@@ -268,18 +275,18 @@ def allow_from(self):
268275
self.log.warning('Failed to resolve hostname "%s". An error occurred: %s', host, error)
269276
else:
270277
for ip in ip_list:
271-
if ip not in allow_from:
278+
if ip not in network_map:
272279
if port:
273-
allow_from[ip] = [port]
280+
network_map[ip] = [port]
274281
else:
275-
allow_from[ip] = None
276-
elif allow_from[ip] is not None:
282+
network_map[ip] = None
283+
elif network_map[ip] is not None:
277284
if port:
278-
allow_from[ip].append(port)
285+
network_map[ip].append(port)
279286
else:
280-
allow_from[ip] = None
287+
network_map[ip] = None
281288

282-
return allow_from
289+
return network_map
283290

284291
@property
285292
@propertycache

0 commit comments

Comments
 (0)