es节点利用ldap做认证

简介

nginx的auth_request模块可以集成第三方的认证,具体原理可以参考大神(freedomkk_qfeng的文章) :用 Nginx 的 auth_request 模块集成 LDAP 认证

本文主要是介绍如何用该模块做es节点的权限控制,es节点目前支持的认证工具(shiled x-pack),前者在5.0以后就废弃了,目前使用的是x-pack,但两者都是收费的,虽然x-pack号称已经开源了部分,但是对接ldap等系统的功能并未开源,但是我们可以根据请求过来的url,做一些处理,提取出索引,操作类型,从而做进一步的权限管控

原理

原理比较简单如下:

1. 先将es节点监听在127.0.0.1:9201上面

2. 自己写一个代理程序监听在0.0.0.0:9200上,由他去接收用户过来的请求

3. 提取出url,ip等信息,做一些权限控制,如果符合,则放行

功能点

1. 基于IP的权限控制,能根据用户IP做控制

2. 用户认证信息集中化管理,只需要更新用户表即可,不用想shiled那样每次新增角色还要修改整个集群所有节点

3. 对接ldap,但同时又不会频繁查询ldap,防止影响到ldap的性能

4. 容器化,降低部署成本

技术选型

nginx 的auth_request模块

auth_request的作用就是在访问指定路径的时候,先去请求某服务,在根据返回的状态码执行特定的操作,比如跳转,放行等,状态码目前仅支持(200/401/403),但是nginx默认是不带该模块的,需要独立安装

安装auth_request

yum安装必要组件


yum -y install geoip ca-certificates geoip-dev pcre libxslt gd gd-dev libxslt-dev libgcc patch \

gcc libc-dev make openssl-dev pcre-dev zlib-dev linux-headers jemalloc-dev

编译

这里使用的nginx版本为:nginx-1.12.2,具体可以在github中看到


tar -xf nginx-1.12.2.tar.gz \

&& cd nginx-1.12.2 \

&& sh configure \

    --prefix=/usr/local/nginx \

    --conf-path=/etc/nginx/nginx.conf \

    --user=nginx \

    --group=nginx \

    --error-log-path=/var/log/nginx/error.log \

    --http-log-path=/var/log/nginx/access.log \

    --pid-path=/var/run/nginx/nginx.pid \

    --lock-path=/var/lock/nginx.lock \

    --with-http_ssl_module \

    --with-http_stub_status_module \

    --with-http_gzip_static_module \

    --with-http_flv_module \

    --with-http_mp4_module \

    --http-client-body-temp-path=/var/tmp/nginx/client \

    --http-proxy-temp-path=/var/tmp/nginx/proxy \

    --http-fastcgi-temp-path=/var/tmp/nginx/fastcgi \

    --with-http_auth_request_module \

&& make \

&& make install 

创建nginx缓存目录

mkdir -p /var/cache/nginx 

mkdir -p /var/tmp/nginx

修改nginx配置文件

#nginx
#user  nobody;
worker_cpu_affinity auto;

#error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;

#pid        logs/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       mime.types;
    default_type  application/octet-stream;
    proxy_cache_path cache/ keys_zone=auth_cache:10m;

    #access_log  logs/access.log  main;
    client_max_body_size 20m;

    sendfile        on;
    #tcp_nopush     on;

    #keepalive_timeout  0;
    keepalive_timeout  65;

    upstream elk_9200 {
        server 127.0.0.1:9201; # ES监听端口
    }
    upstream backend {
        server 127.0.0.1:5000; # 程序监听的端口
    }
    server {
        listen       9200;
        server_name  localhost;

        location / {
            auth_request /auth-proxy; # 请求 auth-proxy这个模块
            error_page 401 @error401; 
            error_page 403 @error403;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            #add_header URI  $request_uri;
            proxy_pass http://elk_9200/;
        }
        location = /auth-proxy {
            internal;
            proxy_set_header X-real-url "$scheme://$server_addr$request_uri"; # 将url写入头部,方便后续的分析
            proxy_set_header X-real-method $request_method; # 请求的方法也写入header
            proxy_set_header X-real-ip   $remote_addr; # 将IP写入header(因为用了 

          #反向代理后程序如果直接使用request.RemoteAddr拿到的永远是127.0.0.1,而且 
          # 用nginx添加的话还能防止用户伪造remote_addr)
            proxy_pass http://backend/auth-proxy; # 请求过来后,先去请求127.0.0.1:5000/auth-proxy 这个url
            proxy_pass_request_body off;
            proxy_set_header Content-Length "";
            # nginx认证缓存配置,如果开启了该配置意味着认证通过后在指定时间内,不会再次发起认证
            #proxy_cache auth_cache; # 是否开启缓存
            
            #proxy_cache_valid 200 10m; # 缓存时间
        }

        location @error401 {
            return 401;
        }
        location @error403 {
            return 403;
        }
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }

    }


}

gin web框架

至于为什么选择gin,个人觉得其社区活跃度是目前最高的,也算是个人习惯问题吧,当然也可以使用其他框架,比如iris,beego等


代码逻辑

func Login(c *gin.Context) {
        // 由于是http basic认证,所以从header中提取出认证信息,并解密
    user, password := utils.DecodeBase64UserInfo(c.Request.Header.Get("Authorization"))
    if user == "" || password == ""{
        logger.Alert("Username or password is Nil.")

        c.AbortWithStatus(401)
        return
    }
        // 将用户认证信息加密后存到缓存中
    md5Password := utils.MD5Encode(password)
    // 如果该用户存在缓存中 则与缓存中的数据进行匹配
    cachePass, err := getCachePass(user)
    if err == nil {
        // 如果与缓存中的密码匹配
        if md5Password == cachePass {
            logger.Info(user, " login from cache success")

        } else {
            logger.Alert(user, "password in cache is wrong")
            c.AbortWithStatus(401)
            return
        }

    } else {
                // 否则就
        // 是否开启了ldap
        if g.LdapEnable {
            
            ldapClient := utils.InitLdap()
            ldapClient.Connect()
            err := ldapClient.Auth(user, password)
            if err != nil {
                logger.Alert("Login ldap failed, user: ",user)
                c.AbortWithStatus(401)
                return
            }
            logger.Info("Login ldap success, write user info into cache...")
        }else {
            userPri := g.RealPrivilege.GetUserPrivileges(user)
            if password != userPri.Password{
                logger.Alert("Login from conf failed, user: ",user)
                c.AbortWithStatus(401)
                return
            }
            logger.Info("Login from conf success, write user info into cache...")
        }
                // 加密后写入缓存
        setUserCache(user, utils.MD5Encode(password))
    }
        // 开始匹配用户
    if !matchACL(user, c){
        c.AbortWithStatus(403)
        return
    }
}

定期清空缓存中的认证信息

/*
定期清空
 */
func DeleteUserInfoCache(){
    for {
        ticker := time.NewTicker( time.Duration(g.GetViper().GetInt("emptyUserCacheCycle")) * time.Second)
        select {
        case <- ticker.C:
            logger.Info("Empty User Info Cache...")
            es.DeleteAllUserCache()
        }
        ticker.Stop()
    }
}

/*
定期同步user_role表中的权限数据到内存中
 */
func SyncUserRoleTable(){
    for {
        ticker := time.NewTicker( time.Duration(g.GetViper().GetInt("syncDBCycle")) * time.Second)
        select {
        case <- ticker.C:
            logger.Info("Sync User Role...")
            g.RealPrivilege.SyncUserRole()
        }
        ticker.Stop()
    }

}

基于uri的权限控制

es的url请求都是很有规律的,但具体研究下来其实也比较复杂,结合日常的使用,能简单的分为:索引类操作,集群类操作 具体的权限分为:只读,编辑,全部(除了编辑只读,还有delete等)

集群类操作

以 "_" 开头的都可以认为是集群类操作

索引类操作

非"_"开头的uri 基本上都能提取出索引

缺陷

有一些请求是索引操作,但是也会被识别为集群,比如bulk,但这类少,目前碰到的也只有bulk,而一般使用bulk都是大批量的写入操作,比如logstash等,这类就应该配置高权限的账户了,因为他要对所有索引进行写入

数据库表设计

CREATE TABLE user_role(
  username varchar(256) , # 用户名
  index varchar(256), # 索引
  indexprivilege varchar(64), # 索引权限
  clusterprivilege varchar(64), # 集群权限
  password         varchar(256) # 密码 如果走ldap该字段无意义
);

比如如下数据的意思是elk 拥有所有索引所有权限,也拥有集群所有权限
注意:索引如果为:ALL 代表是全部索引

不同场景

1.公司无ldap
可以将配置文件的ldap.enable改为false,这样认证就会走数据库或者配置文件,如果走数据库的话密码为password字段
2.不想安装数据库
如果无数据库可以将认证写在配置文件中,但是这样的话和shield类似了,每次新增用户所有节点都要更新
参考:https://www.jianshu.com/p/bfde26b46709
代码仓储:https://github.com/peng19940915/nginx_ldap_auth_tool

你可能感兴趣的:(es节点利用ldap做认证)