ini 在声明性Nginx配置中实现的原始Double A(AAA-minus-Accounting)RBAC系统
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ini 在声明性Nginx配置中实现的原始Double A(AAA-minus-Accounting)RBAC系统相关的知识,希望对你有一定的参考价值。
# Nginx Double A
A primitive Double A (AAA-minus-Accounting) RBAC system implemented in declarative Nginx config.
## Background
So I noticed <https://github.com/alexaandru/elastic_guardian>, a simple AAA reverse-proxy to sit in front of Elasticsearch. Reading the source and comments tickled my "why is this in code not config?" funnybone.
I asked @alexaandru (<https://twitter.com/jpluscplusm/status/438339557906735104>) who told me it was mostly the resulting complexity of the nginx config he tried that prompted him to write it.
Well, I have a bit of a **thing** for implementing things purely in Nginx config which, perhaps, one really *really* shouldn't. People I've worked with previously will, no doubt, be screaming "you're not bloody kidding!" at the screen. My sincere apologies to those I've hurt in the past ... :-)
@alexaandru let me have a summary of the complexities he was dealing with, and this gist is the result.
## Constraints as stated by @alexaandru
1. readonly account to /_cluster/* (restricted to GET requests) - used for monitoring
1. global readonly account (any URLs but only GET)
1. full account (any URLs; any HTTP verbs)
1. accounts limited to specific indexes only (some readonly some read/write)
## My interpretation of the constraints
* There exist 3 system accounts, and many user accounts
* The 3 system accounts are
* "readonly"
* "readonly_global"
* "root"
* The 3 system accounts map to constraints #1, #2 and #3.
* The user accounts, for the purposes of this test, are
* "i1_read"
* "i1_write"
* "i2_read"
* "i2_write"
* The user accounts have access to the testing index prefixes `/index1/` and `/index2/`.
* Here, a "_read" username suffix indicates GET access only
* A "_write" username suffix indicates any HTTP method is permitted
* ... but these naming conventions are **not** used to mediate access as they may not be present in real life.
* To simulate real world complexities, the "i1_write" user is *also* allowed read access to index2.
## Practical considerations
It's fairly obvious that a set of HTTP password files and matching nginx prefix `location{}`s could be configured to allow this access. I suspect the complexities that @alexaandru discovered centre around the fact that this simple nginx setup would have the following drawbacks:
* Each user who needs access to more than one location (e.g. "root" needs access to at least `/index1/` and `/index2/`) will need to be present in all those locations' htpasswd files, with the inherent problems that duplication brings
* You'd need to duplicate a load of `proxy_(pass|set_header|etc)` settings (or use some `include`s) for each and every location which ultimately needed to hit the Elasticsearch backend
* I'm not even sure how you'd authenticate a read-only user for GET but **not** for POST/PUT/DELETE using the normal nginx auth mechanisms.
So, rather than construct a bunch of locations and some interesting config to tie them together, I've come up with the config in this gist. It has the following properties:
* Each username/password combination is stored in exactly one place
* All username/password combinations are stored in one file
* Each user's access groups are specified in one place
* It can be trivially extended to many more users
* It can be trivially extended to many more URI prefixes
* There's nothing Elasticsearch-specific in here: it can be used in front of any HTTP service
* The nginx `server{}` which reverse-proxies to Elasticsearch is extremely simple and uses no advanced nginx features or subtle interactions which would complicate extending or modifying it later.
It suffers from the following disadvantages, however:
* Adding a new index **and allowing many existing users access** requires a fiddly operation to amend those users' access groups. It's reasonably scriptable, but there's no nice way of allowing it by default.
* The regex nginx `map{}` is not simple. The entries are quite cargo-cultable as new groups and indexes are added, but it could look intimidating to new users.
* There's no exclusion mechanism; access is granted based on group membership (see below): if access must not be granted to a user, that user must not be part of the group which is allowed access.
## Implementation
The nginx `server{}` which reverse-proxies to Elasticsearch denies access via a 403 when the `map{}` variable `$request_denied` tells it to. By default, access is denied.
`$request_denied` is driven by a combination of `$request_method`, `$user_groups` and `$request_uri`.
`$user_groups` is a `map{}` which is a @-seperated (and -prefixed and -suffixed) list of the groups to which a user belongs.
Access to a specific URI prefix with a specific HTTP method (or combination of methods) is granted based on membership of a specific group. Different group memberships may allow access to overlapping URI prefixes, and will often reference the same HTTP methods.
Here's the `$user_groups` map:
map $remote_user $user_groups {
default 0;
readonly @monitoring@;
global_readonly @global_ro@;
root @global_rw@;
i1_read @index1_ro@;
i1_write @index1_rw@index2_ro@;
i2_read @index2_ro@;
i2_write @index2_rw@;
}
`@` is used as a separator because it's not a regex metacharacter, which will be handy in `$request_denied`, below. Prefixes and suffixes are used for some semblance of security in the case of maliciously chosen index names leading to string prefix collisions and priviledge escalation.
Here's the `$request_denied` "boolean" map (with its comments removed):
map $request_method:$user_groups:$request_uri $request_denied {
default 1;
~^GET:.*@monitoring@.*:/_cluster/ 0;
~^GET:.*@global_ro@.*:/ 0;
~^[^:]*:.*@global_rw@.*:/ 0;
~^GET:.*@index1_ro@.*:/index1/ 0;
~^[^:]*:.*@index1_rw@.*:/index1/ 0;
~^GET:.*@index2_ro@.*:/index2/ 0;
~^[^:]*:.*@index2_rw@.*:/index2/ 0;
}
Essentially, we check a per-request METHOD:GROUPS:URI tuple against a regex, and allow access in certain cases. The visual nastiness is down to the regex nature of the map. It would be possible to construct a set of cascading nginx `map{}`s which made this final boolean `map{}` much nicer to look at, but I didn't do that.
## Triple A
Adding in logging of allowed/denied requests is left as an exercise for the reader.
## Thanks
* @alexaandru for giving me an interesting late-night challenge to work on
* @Nginx people for giving us a great tool to use. BUT STOP INTERMINGLING THE NGINX/NGINX-PLUS DOCUMENTATION IT'S REALLY REALLY ANNOYING!
map $remote_user $user_groups {
default 0;
readonly @monitoring@;
global_readonly @global_ro@;
root @global_rw@;
i1_read @index1_ro@;
i1_write @index1_rw@index2_ro@;
i2_read @index2_ro@;
i2_write @index2_rw@;
}
map $request_method:$user_groups:$request_uri $request_denied {
# DENY by default
default 1;
# /_cluster/ GET
~^GET:.*@monitoring@.*:/_cluster/ 0;
# / GET
~^GET:.*@global_ro@.*:/ 0;
# / ALL
~^[^:]*:.*@global_rw@.*:/ 0;
# /index1/ GET
~^GET:.*@index1_ro@.*:/index1/ 0;
# /index1/ ALL
~^[^:]*:.*@index1_rw@.*:/index1/ 0;
# /index2/ GET
~^GET:.*@index2_ro@.*:/index2/ 0;
# /index2/ ALL
~^[^:]*:.*@index2_rw@.*:/index2/ 0;
}
server {
listen 80;
server_name es;
location / {
auth_basic "Elasticsearch";
auth_basic_user_file /etc/nginx/passwd;
if ($request_denied) { return 403 "DENIED: user:$remote_user method:$request_method uri:$request_uri user_groups:$user_groups
"; }
proxy_set_header Remote-User $remote_user;
proxy_set_header User-Groups $user_groups;
proxy_set_header Host real-es;
proxy_pass http://127.0.0.1:80;
}
}
# All passwords are "password"
readonly:$apr1$JyI00QAJ$KDPDMzo87ogsVnEq/nxfg0
global_readonly:$apr1$JyI00QAJ$KDPDMzo87ogsVnEq/nxfg0
root:$apr1$JyI00QAJ$KDPDMzo87ogsVnEq/nxfg0
i1_read:$apr1$JyI00QAJ$KDPDMzo87ogsVnEq/nxfg0
i2_read:$apr1$JyI00QAJ$KDPDMzo87ogsVnEq/nxfg0
i1_write:$apr1$JyI00QAJ$KDPDMzo87ogsVnEq/nxfg0
i2_write:$apr1$JyI00QAJ$KDPDMzo87ogsVnEq/nxfg0
# This curl-friendly server simulates the real Elasticsearch backend, as I
# didn't have one knocking around to play with.
server {
listen 80;
server_name real-es;
location / { return 200 "PERMITTED: user:$http_remote_user method:$request_method uri:$request_uri user_class:$http_user_class
"; }
}
以上是关于ini 在声明性Nginx配置中实现的原始Double A(AAA-minus-Accounting)RBAC系统的主要内容,如果未能解决你的问题,请参考以下文章