乐于分享
好东西不私藏

4-11-1-docker访问授权插件

4-11-1-docker访问授权插件

本文档介绍 Docker Engine 内置的插件相关内容。如需查看 Docker Engine 托管插件的相关信息,请参阅《Docker Engine 插件系统》文档。

Docker 开箱即用的授权模型是 “全有或全无” 模式:任何拥有访问 Docker 守护进程权限的用户,都可以执行任意 Docker 客户端命令。通过 Docker Engine API 调用守护进程的调用方也遵循同一规则。若你需要更细粒度的访问控制,可以开发授权插件并将其配置到 Docker 守护进程中。借助授权插件,Docker 管理员能够配置精细化的访问策略,管控对 Docker 守护进程的各类访问行为。

具备对应开发能力的人员均可开发授权插件,基础能力要求包括:熟悉 Docker、理解 REST 接口、扎实的编程功底。本文档面向授权插件开发者,讲解相关架构、状态与接口方法信息。

基本原理

Docker 插件基础设施支持通过通用 API 加载、卸载第三方组件并与之通信,访问授权子系统正是基于这套机制构建。

借助该子系统,你无需重新编译构建 Docker 守护进程即可新增授权插件;插件可直接安装在已部署运行的 Docker 守护进程上,但新增插件后必须重启 Docker 守护进程才能生效。

授权插件会结合认证上下文命令上下文,审批或拒绝发往 Docker 守护进程的请求:

  • 认证上下文:包含用户全部信息与所使用的认证方式
  • 命令上下文:包含请求的全部相关数据

授权插件必须遵循《Docker 插件 API》中规定的规范,所有插件文件必须存放于《插件发现》章节指定的目录内。

备注

缩写释义:AuthZ = 授权(authorization),AuthN = 认证(authentication)

默认用户授权机制

若 Docker 守护进程开启 TLS 功能,默认用户授权流程会从客户端证书的主题名称中提取用户信息:

  • User
     字段取值为客户端证书主题通用名
  • AuthenticationMethod
     字段固定为 TLS

基础架构

你需要在 Docker 守护进程启动阶段注册插件;系统支持安装多个插件并将其串联形成插件链,插件链可自定义执行顺序。所有发往守护进程的请求会按顺序依次经过整条插件链,只有全部插件均放行该资源访问,请求才会最终通过

当客户端通过 CLI 或 Engine API 向 Docker 守护进程发起 HTTP 请求时,认证子系统会将携带调用方用户信息与命令上下文的请求转发给已安装的授权插件,由插件判定允许或拒绝本次请求。

下方时序图分别展示授权通过、授权拒绝两种流程:

授权允许流程

授权拒绝流程

转发至插件的每条请求都会携带已认证用户、HTTP 请求头、请求 / 响应体;插件仅能获取用户名与认证方式,不会收到任何用户凭证或令牌

备注

  1. 授权插件仅管控 Docker 守护进程 HTTP API 的请求;原生 gRPC 调用、或通过 POST /grpc 升级后的 gRPC 调用不受授权插件管控。
  2. 仅 Content-Type: application/json 类型的 HTTP 请求 / 响应体会转发给插件;其他类型的请求 / 响应体插件无法读取,即便守护进程会解析处理这类数据,插件也无法基于其内容执行权限管控。
  3. 对于会劫持 HTTP 连接(HTTP Upgrade)的命令(例如 exec),授权插件仅校验初始 HTTP 请求;插件放行后,后续交互流程不再执行授权校验,流式传输的数据不会转发给插件。
  4. 对于返回分块 HTTP 响应的命令(例如 logsevents),仅 HTTP 请求会发送给授权插件,响应流不会传递给插件。

引擎授权中间件遵循故障即拒绝(fail closed) 策略:插件返回错误、或返回 Allow: false 时,请求直接被拦截,错误信息返回给客户端。插件自身也应当遵循故障即拒绝原则:若插件无法可靠判定请求权限,应返回错误或 Allow: false

警告

插件接收的是守护进程传递的原始请求体,插件必须使用与 Docker 守护进程完全一致的解码逻辑,才能准确判断守护进程实际处理的请求内容;Docker 守护进程使用 Go 标准库 encoding/json.Unmarshal 解析 JSON 数据。

响应体校验也遵循相同要求:若插件需要读取 ResponseBody 实现内容脱敏、过滤等逻辑,仅能针对一次性完整写入返回响应的接口(典型 REST 风格 API)编写权限策略。

对于流式返回、或会多次分段写入、数据量极易超出缓冲区的命令,禁止依赖响应体做安全相关权限判定,这类场景需要在 Docker 守护进程前端额外部署独立过滤层。

响应体大小与部分缓冲

Docker 守护进程 HTTP 处理器与插件响应授权回调(responseModifier,定义路径:pkg/authorization/response.go)之间,用于存放响应体的内部缓冲区固定容量为 64 KiB(对应参数 maxBufferSize)。

绝大多数非流式接口会将完整响应存入缓冲区供插件读取,无论响应总大小;原因是 Go 的 JSON 编码器会把完整载荷一次性写入底层缓冲区。

日志、事件这类流式接口不受该规则约束,本质是 64 KiB 缓冲区上限搭配流式处理器 io.WriteFlusher 写入模式共同导致:每一段数据写入后会立即下发给客户端,处理器执行完毕时缓冲区已无数据,插件无法读取响应内容。

在请求 / 响应处理流程中,部分授权逻辑需要额外调用 Docker 守护进程接口。插件可像普通客户端一样调用守护进程 API 完成这类逻辑,但管理员必须为插件配置对应的认证凭证与安全策略,保障插件可正常发起附加查询。

Docker 客户端流程

插件开发者必须支持本节所述 Docker 客户端交互逻辑,才能完成授权插件的启用与配置。

配置 Docker 守护进程

通过专用命令行参数启用授权插件,参数格式:--authorization-plugin=插件ID,该参数传入的值为插件套接字地址或插件规范文件路径。授权插件无需重启守护进程即可加载,更多细节参阅 dockerd 文档。

$ dockerd --authorization-plugin=plugin1 --authorization-plugin=plugin2,...

Docker 授权子系统支持传入多个 --authorization-plugin 参数配置多条插件。

执行授权通过的命令(允许)

$ docker pull centos<...>f1b10cd84249: Pull complete<...>

执行未授权命令(拒绝)

$ docker pull centos<...>docker: Error response from daemon: authorization denied by plugin PLUGIN_NAME: volumes are not allowed.

插件报错

$ docker pull centos<...>docker: Error response from daemon: plugin PLUGIN_NAME failed with error: AuthZPlugin.AuthZReq: Cannot connect to the Docker daemon. Is the docker daemon running on this host?.

API 规范与实现

除 Docker 标准插件注册方式外,每个授权插件必须实现以下两个接口方法:

  1. /AuthZPlugin.AuthZReq
    :请求授权接口,在 Docker 守护进程处理客户端请求前调用
  2. /AuthZPlugin.AuthZRes
    :响应授权接口,在 Docker 守护进程向客户端返回响应前调用

/AuthZPlugin.AuthZReq

请求报文结构

{    "User":              "用户标识",    "UserAuthNMethod":   "所使用的认证方式",    "RequestMethod":     "HTTP 请求方法",    "RequestURI":        "HTTP 请求 URI",    "RequestBody":       "存放原始 HTTP 请求体的字节数组",    "RequestHeader":     "以 map[string][]string 格式存储原始 HTTP 请求头的字节数组"}

响应报文结构

{    "Allow": "布尔值,标识是否放行该用户请求",    "Msg":   "授权提示信息",    "Err":   "插件执行异常时的错误信息"}

/AuthZPlugin.AuthZRes

请求报文结构

{    "User":              "用户标识",    "UserAuthNMethod":   "所使用的认证方式",    "RequestMethod":     "HTTP 请求方法",    "RequestURI":        "HTTP 请求 URI",    "RequestBody":       "存放原始 HTTP 请求体的字节数组",    "RequestHeader":     "以 map[string][]string 格式存储原始 HTTP 请求头的字节数组",    "ResponseBody":      "存放原始 HTTP 响应体的字节数组",    "ResponseHeader":    "以 map[string][]string 格式存储原始 HTTP 响应头的字节数组",    "ResponseStatusCode":"HTTP 响应状态码"}

响应报文结构

{    "Allow":              "布尔值,标识是否放行本次响应",    "Msg":                "授权提示信息",    "Err":                "插件执行异常时的错误信息"}

请求授权

插件必须支持两类请求授权报文格式:一类是守护进程下发至插件的报文,一类是插件回传给守护进程的报文。下方表格详细说明两类报文包含字段。

守护进程 → 插件

字段名称
数据类型
字段说明
User
string
用户标识
Authentication method
string
请求使用的认证方式
Request method
枚举值
HTTP 请求方法(GET/DELETE/POST)
Request URI
string
客户端发起的完整 HTTP 请求 URI,包含 API 版本(示例:v.1.17/containers/json)
Request headers
map[string]string
键值对格式的请求头(剔除鉴权请求头)
Request body
[]byte
原始请求体字节流

插件 → 守护进程

字段名称
数据类型
字段说明
Allow
bool
布尔值,标记是否放行本次请求
Msg
string
授权提示信息;若访问被拒绝,该信息会返回给客户端
Err
string
插件运行异常时的错误信息;内容会写入日志,禁止携带敏感机密信息

响应授权

插件必须支持两类响应授权报文格式:一类是守护进程下发至插件的报文,一类是插件回传给守护进程的报文。下方表格详细说明两类报文包含字段。

守护进程 → 插件

字段名称
数据类型
字段说明
User
string
用户标识
Authentication method
string
请求使用的认证方式
Request method
string
HTTP 请求方法(GET/DELETE/POST)
Request URI
string
客户端发起的完整 HTTP 请求 URI,包含 API 版本(示例:v.1.17/containers/json)
Request headers
map[string]string
键值对格式的请求头(剔除鉴权请求头)
Request body
[]byte
原始请求体字节流
Response status code
int
Docker 守护进程返回的 HTTP 状态码
Response headers
map[string]string
键值对格式的响应头
Response body
[]byte
Docker 守护进程返回的原始响应体字节流

插件 → 守护进程

字段名称
数据类型
字段说明
Allow
bool
布尔值,标记是否放行本次响应
Msg
string
授权提示信息;若访问被拒绝,该信息会返回给客户端
Err
string
插件运行异常时的错误信息;内容会写入日志,禁止携带敏感机密信息