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系统的主要内容,如果未能解决你的问题,请参考以下文章

这是如何在 swift 4 中实现的?

ini [nginx conf] nginx简易转发配置#nginx

警告:在两个 SDK 中实现的类

ini Nginx最佳配置(基本配置)

ini Nginx最佳配置(基本配置)

安装 BPL 以及在其他 BPL 中实现的组件