简介
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