Consul 源码剖析 - ACL 权限控制模块

1. 模块概览

1.1 职责定义

ACL (Access Control List) 模块是 Consul 的权限控制核心,负责管理和实施基于策略的访问控制。该模块提供细粒度的权限管理,确保只有经过授权的实体才能访问 Consul 的各种资源(服务、节点、KV、配置等)。

核心职责

  • Token 生命周期管理(创建、更新、删除、查询)
  • Policy 策略定义与管理
  • Role 角色管理(将多个 Policy 组合)
  • BindingRule 绑定规则(Auth Method 与 Role 的关联)
  • Auth Method 认证方法(支持 Kubernetes、JWT、OIDC等)
  • 权限解析与强制执行(Resolver 和 Authorizer)
  • Token 复制(跨数据中心)
  • Bootstrap 初始化

1.2 输入与输出

输入

  • HTTP/RPC 请求(携带 ACL Token)
  • 策略定义(HCL/JSON 格式)
  • 认证凭证(JWT、OIDC Token、Kubernetes Service Account)

输出

  • ACL Token(SecretID 和 AccessorID)
  • 授权决策(Allow/Deny/Default)
  • 策略评估结果
  • 错误信息(权限不足、Token 无效等)

1.3 上下游依赖

上游调用方

  • HTTP API 层(agent/http/acl.go)
  • 所有 RPC 端点(每个请求前都需要 ACL 校验)
  • gRPC 服务(外部 peering、resource service)

下游依赖

  • State Store(存储 Token、Policy、Role等)
  • Raft(写操作通过共识)
  • Replication(跨数据中心复制)
  • Auth Method 验证器(JWT、Kubernetes等)

1.4 生命周期

初始化阶段

  1. Server 启动时检查 ACL 配置(ACLsEnabled
  2. 如未 Bootstrap,等待管理员执行 Bootstrap 操作
  3. 初始化 ACL Resolver(缓存 Token 和 Policy)
  4. 启动 Token 复制任务(非主数据中心)

运行时阶段

  1. 接收 Token/Policy/Role 管理请求
  2. 验证请求者权限
  3. 通过 Raft Apply 写入状态
  4. 更新 Resolver 缓存
  5. 复制到其他数据中心(如适用)

关闭阶段

  • 停止 Token 复制任务
  • 清理缓存资源

2. 模块架构图

flowchart TB
    subgraph "HTTP/RPC 层"
        API[HTTP API<br>/v1/acl/*]
        RPC[RPC ACL Endpoint]
    end
    
    subgraph "ACL 核心模块"
        direction TB
        Bootstrap[Bootstrap 初始化]
        TokenMgr[Token 管理器]
        PolicyMgr[Policy 管理器]
        RoleMgr[Role 管理器]
        BindingMgr[BindingRule 管理器]
        AuthMethodMgr[AuthMethod 管理器]
        Login[Login/Logout 登录]
        
        Resolver[ACL Resolver<br>Token+Policy缓存]
        Authorizer[Authorizer<br>权限决策引擎]
    end
    
    subgraph "权限执行点"
        CatalogAuth[Catalog 权限检查]
        KVAuth[KV 权限检查]
        ServiceAuth[Service 权限检查]
        NodeAuth[Node 权限检查]
        OtherAuth[其他模块检查]
    end
    
    subgraph "数据存储"
        FSM[FSM 状态机]
        State[State Store<br>Memdb]
        Raft[Raft 共识]
    end
    
    subgraph "跨DC复制"
        ReplicatorToken[Token Replicator]
        ReplicatorPolicy[Policy Replicator]
    end
    
    API --> RPC
    RPC --> Bootstrap
    RPC --> TokenMgr
    RPC --> PolicyMgr
    RPC --> RoleMgr
    RPC --> BindingMgr
    RPC --> AuthMethodMgr
    RPC --> Login
    
    TokenMgr --> Resolver
    PolicyMgr --> Resolver
    RoleMgr --> Resolver
    Login --> AuthMethodMgr
    Login --> TokenMgr
    
    Resolver --> Authorizer
    
    CatalogAuth --> Authorizer
    KVAuth --> Authorizer
    ServiceAuth --> Authorizer
    NodeAuth --> Authorizer
    OtherAuth --> Authorizer
    
    TokenMgr --> FSM
    PolicyMgr --> FSM
    RoleMgr --> FSM
    BindingMgr --> FSM
    AuthMethodMgr --> FSM
    
    FSM --> Raft
    Raft --> State
    
    State --> ReplicatorToken
    State --> ReplicatorPolicy
    ReplicatorToken --> RPC
    ReplicatorPolicy --> RPC

2.1 架构说明

组件职责

  1. Bootstrap 初始化

    • 作用:首次部署时创建初始管理 Token
    • 边界:只能执行一次,除非通过 Reset Index 重置
    • 状态:无状态,操作记录在 State Store 的 Bootstrap Index
  2. Token 管理器

    • 作用:Token 的 CRUD 操作,包括 Clone(复制)
    • 边界:Token 可以是 Local(本地数据中心)或 Global(全局复制)
    • 状态:Token 存储在 State Store,Resolver 缓存 Token 及其关联的 Policy
  3. Policy 管理器

    • 作用:管理 ACL 策略(HCL 规则)
    • 边界:Policy 始终是全局的,通过 Raft 复制
    • 状态:Policy 存储在 State Store,解析后的策略缓存在 Resolver
  4. Role 管理器

    • 作用:角色是 Policy 和 ServiceIdentity 的集合
    • 边界:Role 全局复制,可关联多个 Policy
    • 状态:Role 存储在 State Store
  5. ACL Resolver

    • 作用:缓存 Token 到 Authorizer 的映射,避免重复解析
    • 边界:本地缓存,TTL 默认 30 秒
    • 状态:内存缓存(LRU),缓存未命中时从 State Store 加载
  6. Authorizer

    • 作用:执行权限判定(Allow/Deny/Default)
    • 边界:无状态,基于 Policy 规则树进行决策
    • 状态:Policy 规则预编译为决策树

关键决策点

  • Token 类型选择:创建 Token 时决定是 Local(仅本 DC)还是 Global(跨 DC 复制)
  • Policy 合并:Token 可关联多个 Policy/Role,Authorizer 按优先级合并(Deny > Allow > Default)
  • 缓存策略:Resolver 使用 TTL 缓存,过期后重新加载;写操作触发缓存失效
  • 复制策略:Global Token 和所有 Policy/Role 通过后台任务从主 DC 复制到从 DC

边界条件

  • 并发写入:Token/Policy 使用 CAS(Compare-And-Swap)防止并发冲突
  • 超时:ACL Resolver 解析超时 5 秒,避免阻塞业务请求
  • 幂等性:相同 AccessorID 的 Token 更新是幂等的(覆盖)
  • 顺序性:复制任务保证顺序性(按 Raft Index 递增)

异常处理

  • Token 不存在:返回 Anonymous Token(AccessorID = 00000000-0000-0000-0000-000000000002)
  • Token 过期:Resolver 自动剔除,后续请求返回权限不足
  • Raft 写入失败:返回错误,客户端可重试
  • 复制延迟:从 DC 可能读到旧 Policy,通过 Blocking Query 等待更新

性能要点

  • Resolver 缓存命中率:生产环境通常 > 95%,减少 State Store 查询
  • Policy 编译:策略首次加载时编译为规则树,后续判定时间复杂度 O(log n)
  • 批量操作:复制任务批量 Apply(最多 100 条/批次),降低 Raft 开销
  • 索引优化:State Store 按 AccessorID、SecretID、Policy ID 建立索引

容量上界

  • 单个 Token 关联 Policy 数量:建议 < 10 个(影响解析性能)
  • 单个 DC 的 Token 总数:< 100,000(受限于 State Store 内存)
  • Policy 规则数量:单个 Policy < 1000 条规则(影响编译时间)

3. 关键数据结构与 UML 图

3.1 核心数据结构类图

classDiagram
    class ACLToken {
        +string AccessorID
        +string SecretID
        +string Description
        +[]ACLTokenPolicyLink Policies
        +[]ACLTokenRoleLink Roles
        +[]ACLServiceIdentity ServiceIdentities
        +[]ACLNodeIdentity NodeIdentities
        +bool Local
        +time.Time CreateTime
        +time.Time ExpirationTime
        +string AuthMethod
        +Hash() []byte
    }
    
    class ACLPolicy {
        +string ID
        +string Name
        +string Description
        +string Rules
        +string Datacenters
        +Hash() []byte
    }
    
    class ACLRole {
        +string ID
        +string Name
        +string Description
        +[]ACLRolePolicyLink Policies
        +[]ACLServiceIdentity ServiceIdentities
        +[]ACLNodeIdentity NodeIdentities
        +Hash() []byte
    }
    
    class ACLBindingRule {
        +string ID
        +string Description
        +string AuthMethod
        +string Selector
        +string BindType
        +string BindName
    }
    
    class ACLAuthMethod {
        +string Name
        +string Type
        +string Description
        +string DisplayName
        +int MaxTokenTTL
        +map Config
    }
    
    class Authorizer {
        <<interface>>
        +ACLRead(ctx) EnforcementDecision
        +ACLWrite(ctx) EnforcementDecision
        +ServiceRead(name, ctx) EnforcementDecision
        +ServiceWrite(name, ctx) EnforcementDecision
        +NodeRead(name, ctx) EnforcementDecision
        +NodeWrite(name, ctx) EnforcementDecision
        +KeyRead(key, ctx) EnforcementDecision
        +KeyWrite(key, ctx) EnforcementDecision
    }
    
    class PolicyAuthorizer {
        +Policy defaultPolicy
        +[]Policy policies
        +AgentRead(name, ctx)
        +AgentWrite(name, ctx)
        +ServiceRead(name, ctx)
        +ServiceWrite(name, ctx)
        +NodeRead(name, ctx)
        +NodeWrite(name, ctx)
        +KeyRead(key, ctx)
        +KeyWrite(key, ctx)
    }
    
    class ACLResolver {
        +resolveToken(secretID) Authorizer
        +resolvePolicies(policyIDs) []Policy
        +cache *lru.TwoQueueCache
        +fetchFromBackend(id)
    }
    
    ACLToken --> ACLPolicy : policies
    ACLToken --> ACLRole : roles
    ACLRole --> ACLPolicy : policies
    ACLBindingRule --> ACLAuthMethod : references
    ACLResolver --> ACLToken : resolves
    ACLResolver --> Authorizer : returns
    PolicyAuthorizer ..|> Authorizer : implements
    ACLToken --> PolicyAuthorizer : compiles to

3.2 数据结构详细说明

3.2.1 ACLToken(令牌)

字段说明

字段 类型 必填 说明 约束
AccessorID string 令牌访问标识符(UUID) 唯一,用于查询和显示,不可用于鉴权
SecretID string 令牌密钥(UUID) 唯一,用于鉴权,需保密
Description string 令牌描述 最大 256 字符
Policies []ACLTokenPolicyLink 关联的策略列表 最多 10 个推荐
Roles []ACLTokenRoleLink 关联的角色列表 最多 10 个推荐
ServiceIdentities []ACLServiceIdentity 服务身份列表 自动生成 Service 读写权限
NodeIdentities []ACLNodeIdentity 节点身份列表 自动生成 Node 读写权限
Local bool 是否本地令牌 true=仅本DC,false=全局复制
CreateTime time.Time 创建时间 自动生成
ExpirationTime time.Time 过期时间 可选,过期后自动失效
AuthMethod string 认证方法名称 通过 Login 创建时自动设置

版本演进

  • v1.4.0 引入 Role 和 ServiceIdentity
  • v1.8.0 引入 NodeIdentity
  • v1.10.0 引入 ExpirationTime(TTL)

序列化:JSON/MessagePack,SecretID 在日志中自动脱敏

3.2.2 ACLPolicy(策略)

字段说明

字段 类型 必填 说明 约束
ID string 策略唯一标识符(UUID) 自动生成
Name string 策略名称 唯一,1-128 字符,仅字母数字和 -_
Description string 策略描述 最大 256 字符
Rules string 策略规则(HCL 格式) 参见策略语法
Datacenters []string 适用数据中心列表 空表示全部 DC

策略规则示例

# 允许读取所有服务
service_prefix "" {
  policy = "read"
}

# 允许写入 web 服务
service "web" {
  policy = "write"
  intentions = "write"
}

# 允许读取 KV 前缀
key_prefix "config/" {
  policy = "read"
}

# 拒绝删除 KV
key_prefix "config/production/" {
  policy = "deny"
}

# 允许节点注册
node_prefix "" {
  policy = "write"
}

规则合并逻辑

  • 同一资源多个规则按顺序评估
  • deny > write > read > list
  • 前缀匹配:最长匹配优先

3.2.3 ACLRole(角色)

字段说明

字段 类型 必填 说明 约束
ID string 角色唯一标识符(UUID) 自动生成
Name string 角色名称 唯一,1-128 字符
Description string 角色描述 最大 256 字符
Policies []ACLRolePolicyLink 关联的策略列表 通过 Policy ID 或 Name 引用
ServiceIdentities []ACLServiceIdentity 服务身份列表 简化服务权限配置
NodeIdentities []ACLNodeIdentity 节点身份列表 简化节点权限配置

使用场景

  • 将多个 Policy 组合为可复用的角色(如 developeroperator
  • 简化 Token 配置,一个 Token 关联一个 Role 即可获得所有权限

3.2.4 Authorizer 接口(权限判定)

核心方法

type Authorizer interface {
    // ACL 管理权限
    ACLRead(*AuthorizerContext) EnforcementDecision
    ACLWrite(*AuthorizerContext) EnforcementDecision
    
    // 服务权限
    ServiceRead(name string, ctx *AuthorizerContext) EnforcementDecision
    ServiceWrite(name string, ctx *AuthorizerContext) EnforcementDecision
    
    // 节点权限
    NodeRead(name string, ctx *AuthorizerContext) EnforcementDecision
    NodeWrite(name string, ctx *AuthorizerContext) EnforcementDecision
    
    // KV 权限
    KeyRead(key string, ctx *AuthorizerContext) EnforcementDecision
    KeyWrite(key string, ctx *AuthorizerContext) EnforcementDecision
    KeyList(prefix string, ctx *AuthorizerContext) EnforcementDecision
    
    // 其他权限(省略详细列表)
    EventRead(name string, ctx *AuthorizerContext) EnforcementDecision
    IntentionRead(name string, ctx *AuthorizerContext) EnforcementDecision
    SessionRead(node string, ctx *AuthorizerContext) EnforcementDecision
    PreparedQueryRead(name string, ctx *AuthorizerContext) EnforcementDecision
    // ...
}

type EnforcementDecision int
const (
    Deny    EnforcementDecision = iota  // 明确拒绝
    Allow                                // 明确允许
    Default                              // 无明确规则,使用默认策略
)

决策流程

  1. 遍历 Token 关联的所有 Policy
  2. 对目标资源按前缀匹配规则
  3. 合并多个策略结果(Deny 优先)
  4. 返回最终决策

4. 核心 API 详细规格

4.1 BootstrapTokens - ACL 初始化

4.1.1 基本信息

  • 名称ACL.BootstrapTokens
  • 协议:RPC 方法
  • HTTP 映射PUT /v1/acl/bootstrap
  • 幂等性:否(仅可执行一次,除非使用 Reset Index)

4.1.2 请求结构体

type ACLInitialTokenBootstrapRequest struct {
    BootstrapSecret string // 可选的预定义 SecretID
    Datacenter      string // 目标数据中心
    QueryOptions           // 查询选项(Token、Consistency等)
}

字段表

字段 类型 必填 默认 约束 说明
BootstrapSecret string 自动生成UUID 36字符UUID格式 指定初始 Token 的 SecretID
Datacenter string 本地DC - 必须是主数据中心

4.1.3 响应结构体

type ACLToken struct {
    AccessorID  string
    SecretID    string
    Description string
    Policies    []ACLTokenPolicyLink
    Local       bool
    CreateTime  time.Time
    // 其他字段见数据结构章节
}

字段表

字段 类型 说明
AccessorID string 生成的 Token AccessorID(UUID)
SecretID string 生成的 Token SecretID(客户端用于鉴权)
Description string “Bootstrap Token (Global Management)”
Policies []ACLTokenPolicyLink 包含 global-management 策略
Local bool false(全局 Token)

4.1.4 入口函数与核心代码

func (a *ACL) BootstrapTokens(
    args *structs.ACLInitialTokenBootstrapRequest,
    reply *structs.ACLToken,
) error {
    // 1. 前置校验
    if !a.srv.config.ACLsEnabled {
        return acl.ErrDisabled
    }
    
    // 2. 转发到 Leader(如非 Leader)
    if done, err := a.srv.ForwardRPC("ACL.BootstrapTokens", args, reply); done {
        return err
    }
    
    // 3. 验证是否可以 Bootstrap
    state := a.srv.fsm.State()
    allowed, resetIdx, err := state.CanBootstrapACLToken()
    if !allowed {
        // 检查是否有 Reset Index 文件
        specifiedIndex := a.fileBootstrapResetIndex()
        if specifiedIndex != resetIdx {
            return fmt.Errorf("ACL bootstrap no longer allowed (reset index: %d)", resetIdx)
        }
    }
    
    // 4. 生成 Token
    accessor, _ := lib.GenerateUUID(a.srv.checkTokenUUID)
    secret := args.BootstrapSecret
    if secret == "" {
        secret, _ = lib.GenerateUUID(a.srv.checkTokenUUID)
    }
    
    token := &structs.ACLToken{
        AccessorID:  accessor,
        SecretID:    secret,
        Description: "Bootstrap Token (Global Management)",
        Policies: []structs.ACLTokenPolicyLink{
            {ID: structs.ACLPolicyGlobalManagementID}, // builtin/global-management
        },
        CreateTime: time.Now(),
        Local:      false,
    }
    
    // 5. 通过 Raft Apply 持久化
    req := &structs.ACLTokenBootstrapRequest{
        Token:      *token,
        ResetIndex: resetIdx,
    }
    
    resp, err := a.srv.raftApply(structs.ACLBootstrapRequestType, req)
    if err != nil {
        return err
    }
    
    // 6. 删除 Reset Index 文件(防止重复 Bootstrap)
    a.removeBootstrapResetFile()
    
    *reply = *token
    return nil
}

代码逻辑解释

  1. 步骤 1-2:校验 ACL 是否启用,非 Leader 转发到 Leader
  2. 步骤 3:检查 State Store 的 BootstrapIndex,已 Bootstrap 则检查 Reset Index 文件
  3. 步骤 4:生成 Token,关联内置的 global-management 策略(具有所有权限)
  4. 步骤 5:通过 Raft 提交,FSM Apply 时更新 BootstrapIndex 防止重复执行
  5. 步骤 6:删除 Reset Index 文件,防止快照恢复后意外重新 Bootstrap

4.1.5 调用链路

HTTP PUT /v1/acl/bootstrap
agent/http/acl.go: aclBootstrapHandler()
agent/rpc.go: RPC("ACL.BootstrapTokens", ...)
agent/consul/acl_endpoint.go: ACL.BootstrapTokens()
agent/consul/server.go: raftApply(ACLBootstrapRequestType, ...)
agent/consul/fsm/fsm.go: FSM.Apply()
agent/consul/fsm/acl.go: applyACLTokenBootstrap()
agent/consul/state/acl.go: ACLBootstrap()
State Store 更新 BootstrapIndex 和 Token 表

4.1.6 时序图

sequenceDiagram
    autonumber
    participant Client as 管理员
    participant HTTP as HTTP API
    participant RPC as ACL RPC Endpoint
    participant Leader as Raft Leader
    participant FSM as FSM 状态机
    participant State as State Store

    Note over Client,State: Bootstrap 初始化流程
    
    Client->>HTTP: PUT /v1/acl/bootstrap
    HTTP->>RPC: ACL.BootstrapTokens()
    
    alt 非 Leader
        RPC->>Leader: 转发到 Leader
    end
    
    RPC->>State: CanBootstrapACLToken()
    State-->>RPC: allowed=true/false, resetIdx
    
    alt Bootstrap 已执行
        RPC->>RPC: 检查 Reset Index 文件
        alt Reset Index 不匹配
            RPC-->>Client: 错误:Bootstrap 不再允许
        end
    end
    
    RPC->>RPC: 生成 AccessorID 和 SecretID
    RPC->>RPC: 构造 Token(关联 global-management 策略)
    
    RPC->>Leader: raftApply(ACLBootstrapRequestType, token)
    Leader->>FSM: Apply Raft Log
    FSM->>State: ACLBootstrap(token)
    State->>State: 更新 BootstrapIndex<br>插入 Token 到 tokens 表
    State-->>FSM: 成功
    FSM-->>Leader: 成功
    Leader-->>RPC: 成功
    
    RPC->>RPC: 删除 Reset Index 文件
    RPC-->>HTTP: 返回 Token
    HTTP-->>Client: 200 OK + Token JSON
    
    Note over Client: 使用 SecretID 进行后续操作

4.1.7 边界与异常

边界条件

  • 首次执行:State Store 的 BootstrapIndex 为 0,允许 Bootstrap
  • 重复执行BootstrapIndex > 0,拒绝请求,返回当前 Index 用于 Reset
  • Reset Index:管理员在 <data_dir>/acl-bootstrap-reset 文件中写入 Reset Index,允许再次 Bootstrap
  • 多 DC:只能在主数据中心执行,从数据中心通过复制获取 Token

异常处理

  • ACL 未启用:返回 acl.ErrDisabled
  • 非主 DC:返回 acl.ErrDisabled
  • 并发 Bootstrap:Raft 串行化,只有一个请求成功,其他返回错误
  • Reset Index 不匹配:返回错误,显示当前正确的 Reset Index

实践建议

  • Bootstrap 后立即保存 SecretID(仅显示一次)
  • 使用 Bootstrap Token 创建更细粒度的管理员 Token
  • 定期轮换 Bootstrap Token(创建新管理员 Token 后删除旧的)
  • 在配置管理工具中预设 BootstrapSecret,实现幂等部署

4.2 TokenSet - 创建/更新 Token

4.2.1 基本信息

  • 名称ACL.TokenSet
  • 协议:RPC 方法
  • HTTP 映射PUT /v1/acl/token
  • 幂等性:是(相同 AccessorID 覆盖)

4.2.2 请求结构体

type ACLTokenSetRequest struct {
    ACLToken   ACLToken // Token 对象(包含所有字段)
    Create     bool     // true=创建,false=更新
    Datacenter string   // 目标数据中心
    WriteRequest        // Token、Namespace 等
}

字段表

字段 类型 必填 默认 约束 说明
ACLToken ACLToken - 见 ACLToken 结构 要创建/更新的 Token
Create bool - - 创建时必须为 true,更新时为 false
Datacenter string 本地DC - 目标数据中心

ACLToken 子字段(创建时):

字段 必填 说明
AccessorID 自动生成(更新时必填)
SecretID 自动生成(可指定)
Description Token 描述
Policies 关联的 Policy ID 或 Name 列表
Roles 关联的 Role ID 或 Name 列表
ServiceIdentities 服务身份列表
Local 默认 false(全局)
ExpirationTTL 过期时间(Duration 格式,如 “24h”)

4.2.3 响应结构体

type ACLToken struct {
    AccessorID     string
    SecretID       string
    Description    string
    Policies       []ACLTokenPolicyLink
    Roles          []ACLTokenRoleLink
    Local          bool
    CreateTime     time.Time
    ExpirationTime time.Time
    Hash           []byte
    CreateIndex    uint64
    ModifyIndex    uint64
}

4.2.4 入口函数与核心代码

func (a *ACL) TokenSet(
    args *structs.ACLTokenSetRequest,
    reply *structs.ACLToken,
) error {
    // 1. 前置校验
    if err := a.aclPreCheck(); err != nil {
        return err
    }
    
    // 2. 权限校验:需要 acl:write 权限
    authz, err := a.srv.ResolveTokenAndDefaultMeta(args.Token, &args.ACLToken.EnterpriseMeta, nil)
    if err != nil {
        return err
    }
    if authz.ACLWrite(nil) != acl.Allow {
        return acl.ErrPermissionDenied
    }
    
    // 3. 转发到 Leader
    if done, err := a.srv.ForwardRPC("ACL.TokenSet", args, reply); done {
        return err
    }
    
    // 4. 验证 Token 字段
    if args.Create {
        // 创建时生成 ID
        if args.ACLToken.AccessorID == "" {
            accessor, _ := lib.GenerateUUID(a.srv.checkTokenUUID)
            args.ACLToken.AccessorID = accessor
        }
        if args.ACLToken.SecretID == "" {
            secret, _ := lib.GenerateUUID(a.srv.checkTokenUUID)
            args.ACLToken.SecretID = secret
        }
        args.ACLToken.CreateTime = time.Now()
    } else {
        // 更新时验证 AccessorID 存在
        if args.ACLToken.AccessorID == "" {
            return fmt.Errorf("AccessorID is required for token update")
        }
        // 从 State Store 读取旧 Token
        state := a.srv.fsm.State()
        _, existing, err := state.ACLTokenGetByAccessor(nil, args.ACLToken.AccessorID, nil)
        if err != nil || existing == nil {
            return fmt.Errorf("Token not found")
        }
        // 保留 CreateTime 和 SecretID
        args.ACLToken.CreateTime = existing.CreateTime
        if args.ACLToken.SecretID == "" {
            args.ACLToken.SecretID = existing.SecretID
        }
    }
    
    // 5. 处理 ExpirationTTL(转换为 ExpirationTime)
    if args.ACLToken.ExpirationTTL != 0 {
        args.ACLToken.ExpirationTime = time.Now().Add(args.ACLToken.ExpirationTTL)
    }
    
    // 6. 验证 Policy/Role 引用存在
    if err := a.validateTokenLinks(&args.ACLToken); err != nil {
        return err
    }
    
    // 7. 通过 Raft Apply
    req := &structs.ACLTokenBatchSetRequest{
        Tokens: structs.ACLTokens{&args.ACLToken},
        CAS:    false,
    }
    
    resp, err := a.srv.raftApply(structs.ACLTokenSetRequestType, req)
    if err != nil {
        return err
    }
    
    // 8. 刷新 ACL Resolver 缓存
    if _, token, err := a.srv.fsm.State().ACLTokenGetByAccessor(nil, args.ACLToken.AccessorID, nil); err == nil && token != nil {
        *reply = *token
    }
    
    return nil
}

代码逻辑解释

  1. 步骤 1-2:检查 ACL 启用,验证请求者具有 acl:write 权限
  2. 步骤 3:非 Leader 转发
  3. 步骤 4:创建时生成 ID 和 SecretID,更新时保留原值
  4. 步骤 5:处理 TTL,转换为绝对过期时间
  5. 步骤 6:验证引用的 Policy/Role 存在于 State Store
  6. 步骤 7:Raft Apply 写入
  7. 步骤 8:返回最终 Token(包括 CreateIndex/ModifyIndex)

4.2.5 时序图

sequenceDiagram
    autonumber
    participant Client as 客户端
    participant HTTP as HTTP API
    participant RPC as ACL RPC Endpoint
    participant Resolver as ACL Resolver
    participant Leader as Raft Leader
    participant FSM as FSM
    participant State as State Store

    Client->>HTTP: PUT /v1/acl/token<br>{"Description": "App Token", "Policies": [...]}
    HTTP->>RPC: ACL.TokenSet(request)
    
    RPC->>Resolver: ResolveToken(请求者 Token)
    Resolver-->>RPC: Authorizer
    RPC->>RPC: 检查 acl:write 权限
    
    alt 权限不足
        RPC-->>Client: 403 Permission Denied
    end
    
    RPC->>RPC: 生成 AccessorID, SecretID<br>设置 CreateTime
    RPC->>State: 验证 Policy/Role 引用存在
    State-->>RPC: OK
    
    RPC->>Leader: raftApply(ACLTokenSetRequestType, token)
    Leader->>FSM: Apply Log
    FSM->>State: ACLTokenSet(token)
    State->>State: 插入/更新 tokens 表<br>更新索引
    State-->>FSM: 成功
    FSM-->>Leader: 成功
    Leader-->>RPC: 成功
    
    RPC->>State: ACLTokenGetByAccessor(AccessorID)
    State-->>RPC: 完整 Token(含 Index)
    
    RPC-->>HTTP: Token
    HTTP-->>Client: 200 OK + Token JSON
    
    Note over Resolver: 后台任务:Token 进入缓存

4.3 TokenRead - 读取 Token

4.3.1 基本信息

  • 名称ACL.TokenRead
  • 协议:RPC 方法
  • HTTP 映射GET /v1/acl/token/:accessor-id
  • 幂等性:是

4.3.2 请求结构体

type ACLTokenGetRequest struct {
    TokenID     string         // AccessorID 或 SecretID
    TokenIDType ACLTokenIDType // "accessor" 或 "secret"
    Expanded    bool           // 是否展开 Policy 内容
    Datacenter  string
    acl.EnterpriseMeta
    QueryOptions
}

字段表

字段 类型 必填 说明
TokenID string Token 的 AccessorID 或 SecretID
TokenIDType ACLTokenIDType “accessor” 或 “secret”
Expanded bool true 时返回 Policy 完整内容

4.3.3 核心代码

func (a *ACL) TokenRead(
    args *structs.ACLTokenGetRequest,
    reply *structs.ACLTokenResponse,
) error {
    // 1. 权限校验:读取自己的 Token 或具有 acl:read 权限
    authz, err := a.srv.ResolveTokenAndDefaultMeta(args.Token, &args.EnterpriseMeta, nil)
    if err != nil {
        return err
    }
    
    // 2. 从 State Store 读取
    state := a.srv.fsm.State()
    var token *structs.ACLToken
    
    if args.TokenIDType == structs.ACLTokenAccessor {
        _, token, err = state.ACLTokenGetByAccessor(nil, args.TokenID, &args.EnterpriseMeta)
    } else {
        _, token, err = state.ACLTokenGetBySecret(nil, args.TokenID, &args.EnterpriseMeta)
    }
    
    if err != nil || token == nil {
        return acl.ErrNotFound
    }
    
    // 3. 权限判定:读取他人 Token 需要 acl:read
    if args.Token != token.SecretID && authz.ACLRead(nil) != acl.Allow {
        return acl.ErrPermissionDenied
    }
    
    // 4. 如果 Expanded=true,填充 Policy 内容
    if args.Expanded {
        for i := range token.Policies {
            _, policy, _ := state.ACLPolicyGetByID(nil, token.Policies[i].ID, &args.EnterpriseMeta)
            if policy != nil {
                token.Policies[i].Policy = policy
            }
        }
    }
    
    reply.Token = token
    reply.Index = token.ModifyIndex
    return nil
}

4.4 TokenDelete - 删除 Token

4.4.1 基本信息

  • 名称ACL.TokenDelete
  • 协议:RPC 方法
  • HTTP 映射DELETE /v1/acl/token/:accessor-id
  • 幂等性:是(重复删除返回成功)

4.4.2 请求结构体

type ACLTokenDeleteRequest struct {
    TokenID    string // Token 的 AccessorID
    Datacenter string
    acl.EnterpriseMeta
    WriteRequest
}

4.4.3 核心代码

func (a *ACL) TokenDelete(
    args *structs.ACLTokenDeleteRequest,
    reply *string,
) error {
    // 1. 权限校验:acl:write
    authz, err := a.srv.ResolveTokenAndDefaultMeta(args.Token, &args.EnterpriseMeta, nil)
    if authz.ACLWrite(nil) != acl.Allow {
        return acl.ErrPermissionDenied
    }
    
    // 2. 禁止删除内置 Token
    if args.TokenID == structs.ACLTokenAnonymousID {
        return fmt.Errorf("Cannot delete anonymous token")
    }
    
    // 3. 通过 Raft Apply 删除
    req := &structs.ACLTokenBatchDeleteRequest{
        TokenIDs: []string{args.TokenID},
    }
    
    _, err = a.srv.raftApply(structs.ACLTokenDeleteRequestType, req)
    if err != nil {
        return err
    }
    
    // 4. 刷新 Resolver 缓存
    a.srv.ACLResolver.cache.Remove(args.TokenID)
    
    *reply = args.TokenID
    return nil
}

4.5 Login - 认证方法登录

4.5.1 基本信息

  • 名称ACL.Login
  • 协议:RPC 方法
  • HTTP 映射POST /v1/acl/login
  • 幂等性:否(每次生成新 Token)

4.5.2 请求结构体

type ACLLoginRequest struct {
    Auth       *ACLLoginParams // 认证参数
    Datacenter string
    WriteRequest
}

type ACLLoginParams struct {
    AuthMethod  string            // Auth Method 名称
    BearerToken string            // JWT/OIDC Token
    Meta        map[string]string // 附加元数据
    acl.EnterpriseMeta
}

字段表

字段 类型 必填 说明
AuthMethod string 认证方法名称(如 “k8s-auth”)
BearerToken string JWT Token 或 OIDC ID Token
Meta map[string]string 附加到 Token Description 的元数据

4.5.3 响应结构体

type ACLToken struct {
    AccessorID  string
    SecretID    string
    Description string
    AuthMethod  string // 设置为登录的 Auth Method
    Roles       []ACLTokenRoleLink // 根据 BindingRule 匹配的 Role
    Local       bool   // true(Auth Method Token 始终是 Local)
    ExpirationTime time.Time // 根据 AuthMethod.MaxTokenTTL 设置
    // ...
}

4.5.4 入口函数与核心代码

func (a *ACL) Login(
    args *structs.ACLLoginRequest,
    reply *structs.ACLToken,
) error {
    // 1. 前置校验
    if !a.srv.LocalTokensEnabled() {
        return errAuthMethodsRequireTokenReplication
    }
    
    // 2. 加载 Auth Method 配置
    authMethod, validator, err := a.srv.loadAuthMethod(
        args.Auth.AuthMethod,
        &args.Auth.EnterpriseMeta,
    )
    if err != nil {
        return err
    }
    
    // 3. 验证 Bearer Token
    verifiedIdentity, err := validator.ValidateLogin(
        context.Background(),
        args.Auth.BearerToken,
    )
    if err != nil {
        return fmt.Errorf("Auth failed: %w", err)
    }
    
    // 4. 查询匹配的 Binding Rules
    state := a.srv.fsm.State()
    _, rules, err := state.ACLBindingRuleList(nil, args.Auth.AuthMethod, &args.Auth.EnterpriseMeta)
    if err != nil {
        return err
    }
    
    var roleLinks []structs.ACLTokenRoleLink
    for _, rule := range rules {
        // 使用 Selector 表达式匹配 Identity 字段
        if matches, _ := evaluateSelector(rule.Selector, verifiedIdentity); matches {
            if rule.BindType == "role" {
                roleLinks = append(roleLinks, structs.ACLTokenRoleLink{
                    Name: interpolate(rule.BindName, verifiedIdentity),
                })
            }
        }
    }
    
    if len(roleLinks) == 0 {
        return fmt.Errorf("No matching binding rules")
    }
    
    // 5. 创建 Token
    accessor, _ := lib.GenerateUUID(a.srv.checkTokenUUID)
    secret, _ := lib.GenerateUUID(a.srv.checkTokenUUID)
    
    description := fmt.Sprintf("token created via login: %s", args.Auth.AuthMethod)
    if len(args.Auth.Meta) > 0 {
        description += fmt.Sprintf(" (%v)", args.Auth.Meta)
    }
    
    token := &structs.ACLToken{
        AccessorID:  accessor,
        SecretID:    secret,
        Description: description,
        Roles:       roleLinks,
        AuthMethod:  args.Auth.AuthMethod,
        Local:       true, // Auth Method Token 始终是 Local
        CreateTime:  time.Now(),
    }
    
    // 6. 设置过期时间
    if authMethod.MaxTokenTTL > 0 {
        token.ExpirationTime = time.Now().Add(authMethod.MaxTokenTTL)
    }
    
    // 7. Raft Apply
    req := &structs.ACLTokenBatchSetRequest{
        Tokens: structs.ACLTokens{token},
    }
    _, err = a.srv.raftApply(structs.ACLTokenSetRequestType, req)
    if err != nil {
        return err
    }
    
    *reply = *token
    return nil
}

代码逻辑解释

  1. 步骤 1:检查 Token 复制是否启用(Auth Method 需要)
  2. 步骤 2:加载 Auth Method 配置和对应的验证器(JWT/Kubernetes/OIDC)
  3. 步骤 3:调用验证器验证 Bearer Token,返回 Identity(包含 claims)
  4. 步骤 4:查询所有 Binding Rules,使用 Selector 表达式匹配 Identity
  5. 步骤 5:根据匹配的规则生成 Role 列表
  6. 步骤 6:创建 Token(Local=true,带过期时间)
  7. 步骤 7:通过 Raft 持久化

4.5.5 时序图

sequenceDiagram
    autonumber
    participant Client as 应用/用户
    participant HTTP as HTTP API
    participant RPC as ACL RPC Endpoint
    participant AuthMethod as Auth Method 验证器
    participant State as State Store
    participant Leader as Raft Leader
    participant FSM as FSM

    Client->>HTTP: POST /v1/acl/login<br>{"AuthMethod": "k8s-auth", "BearerToken": "..."}
    HTTP->>RPC: ACL.Login(request)
    
    RPC->>State: 读取 Auth Method 配置
    State-->>RPC: AuthMethod + Config
    
    RPC->>AuthMethod: ValidateLogin(BearerToken)
    AuthMethod->>AuthMethod: 验证 JWT 签名<br>提取 Claims
    AuthMethod-->>RPC: VerifiedIdentity (claims)
    
    RPC->>State: ACLBindingRuleList(AuthMethod)
    State-->>RPC: BindingRules[]
    
    loop 每个 Binding Rule
        RPC->>RPC: 评估 Selector 表达式<br>匹配 Identity 字段
        alt Selector 匹配
            RPC->>RPC: 插值 BindName(如 "web-${serviceaccount.name}")<br>生成 Role 引用
        end
    end
    
    RPC->>RPC: 生成 Token<br>关联匹配的 Roles<br>设置 ExpirationTime
    
    RPC->>Leader: raftApply(ACLTokenSetRequestType, token)
    Leader->>FSM: Apply Log
    FSM->>State: ACLTokenSet(token)
    State-->>FSM: 成功
    FSM-->>RPC: 成功
    
    RPC-->>HTTP: Token (AccessorID + SecretID)
    HTTP-->>Client: 200 OK + Token JSON
    
    Note over Client: 使用 SecretID 调用 Consul API

4.5.6 Auth Method 配置示例

Kubernetes Auth Method

{
  "Name": "k8s-auth",
  "Type": "kubernetes",
  "Description": "Kubernetes Service Account Auth",
  "MaxTokenTTL": "8h",
  "Config": {
    "Host": "https://kubernetes.default.svc",
    "CACert": "<base64-ca-cert>",
    "ServiceAccountJWT": "<token-reviewer-jwt>"
  }
}

Binding Rule 示例

{
  "AuthMethod": "k8s-auth",
  "Selector": "serviceaccount.namespace==default",
  "BindType": "role",
  "BindName": "web-role"
}

完整 Login 流程示例

# 1. Kubernetes Pod 内获取 Service Account Token
K8S_TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)

# 2. 调用 Consul Login
curl -X POST http://consul:8500/v1/acl/login \
  -d '{
    "AuthMethod": "k8s-auth",
    "BearerToken": "'$K8S_TOKEN'"
  }'

# 响应:
{
  "AccessorID": "xxxx-xxxx-xxxx-xxxx",
  "SecretID": "yyyy-yyyy-yyyy-yyyy",
  "Description": "token created via login: k8s-auth",
  "Roles": [
    {"ID": "...", "Name": "web-role"}
  ],
  "AuthMethod": "k8s-auth",
  "Local": true,
  "ExpirationTime": "2024-10-26T08:00:00Z"
}

# 3. 使用 SecretID 调用其他 API
curl -H "X-Consul-Token: yyyy-yyyy-yyyy-yyyy" \
  http://consul:8500/v1/catalog/services

5. 关键流程与时序图

5.1 Token 权限解析流程

sequenceDiagram
    autonumber
    participant Client as 客户端请求
    participant RPC as RPC Endpoint
    participant Resolver as ACL Resolver
    participant Cache as Token Cache
    participant State as State Store
    participant Authorizer as Authorizer

    Client->>RPC: RPC 请求 (携带 Token)
    RPC->>Resolver: ResolveToken(secretID)
    
    Resolver->>Cache: 查询缓存 (key=secretID)
    
    alt 缓存命中
        Cache-->>Resolver: Authorizer (cached)
    else 缓存未命中
        Resolver->>State: ACLTokenGetBySecret(secretID)
        State-->>Resolver: Token + Policies + Roles
        
        Resolver->>State: 批量读取 Policy 内容
        State-->>Resolver: Policy Rules (HCL)
        
        Resolver->>Resolver: 编译 Policy 为规则树
        Resolver->>Authorizer: 创建 PolicyAuthorizer
        
        Resolver->>Cache: 存入缓存 (TTL=30s)
    end
    
    Resolver-->>RPC: Authorizer
    
    RPC->>Authorizer: ServiceWrite("web", ctx)
    Authorizer->>Authorizer: 查找 "web" 的 service 规则<br>评估 Policy(Deny > Write > Read)
    Authorizer-->>RPC: Allow / Deny
    
    alt 权限不足
        RPC-->>Client: 403 Permission Denied
    else 权限充足
        RPC->>RPC: 执行业务逻辑
        RPC-->>Client: 200 OK + 响应数据
    end

流程说明

  1. 步骤 1-3:所有 RPC 请求携带 Token(Header 或请求参数),Endpoint 调用 Resolver 解析
  2. 步骤 4-5:Resolver 首先查询内存缓存(LRU,容量 1024,TTL 30秒)
  3. 步骤 6-11:缓存未命中时,从 State Store 读取 Token 及其关联的 Policy/Role,编译 Policy Rules 为决策树,创建 Authorizer 并缓存
  4. 步骤 13-14:RPC Endpoint 调用 Authorizer 的具体方法(如 ServiceWrite),Authorizer 在规则树中查找匹配规则,返回决策
  5. 步骤 15-17:根据决策结果,拒绝请求或继续执行

性能优化

  • 缓存避免重复解析,P99 延迟 < 1ms
  • Policy 编译一次,后续判定 O(log n)
  • 批量读取 Policy,减少 State Store 查询次数

5.2 ACL 复制流程(跨数据中心)

sequenceDiagram
    autonumber
    participant PrimaryDC as 主数据中心
    participant PrimaryState as 主 DC State Store
    participant ReplicatorToken as Token Replicator<br>(从DC)
    participant ReplicatorPolicy as Policy Replicator<br>(从DC)
    participant SecondaryRPC as 从 DC RPC
    participant SecondaryState as 从 DC State Store

    Note over PrimaryDC,SecondaryState: 初始化:从 DC 启动复制任务
    
    ReplicatorPolicy->>PrimaryDC: RPC ACL.PolicyList (blocking query)
    PrimaryDC->>PrimaryState: 读取所有 Policy (Index > lastIndex)
    PrimaryState-->>PrimaryDC: Policies[]
    PrimaryDC-->>ReplicatorPolicy: Policies[] + Index
    
    ReplicatorPolicy->>ReplicatorPolicy: 对比本地 Policy<br>计算差异(新增、更新、删除)
    
    loop 批量写入(100条/批次)
        ReplicatorPolicy->>SecondaryRPC: raftApply(ACLPolicyBatchSetRequest, policies)
        SecondaryRPC->>SecondaryState: 批量插入/更新 Policy
    end
    
    ReplicatorPolicy->>ReplicatorPolicy: 更新 lastIndex<br>休眠 1s 后继续
    
    Note over PrimaryDC,SecondaryState: Token 复制(仅 Global Token)
    
    ReplicatorToken->>PrimaryDC: RPC ACL.TokenList (IncludeGlobal=true, Index > lastIndex)
    PrimaryDC->>PrimaryState: 读取 Global Token (Local=false)
    PrimaryState-->>PrimaryDC: Tokens[]
    PrimaryDC-->>ReplicatorToken: Tokens[] + Index
    
    ReplicatorToken->>ReplicatorToken: 对比本地 Token<br>计算差异
    
    loop 批量写入
        ReplicatorToken->>SecondaryRPC: raftApply(ACLTokenBatchSetRequest, tokens)
        SecondaryRPC->>SecondaryState: 批量插入/更新 Token
    end
    
    ReplicatorToken->>ReplicatorToken: 更新 lastIndex<br>休眠 1s 后继续
    
    Note over PrimaryDC,SecondaryState: 持续循环直到进程终止

复制说明

  1. Policy 复制:所有 Policy 都是全局的,从主 DC 复制到所有从 DC
  2. Token 复制:只复制 Local=false 的 Global Token,Local Token 不复制
  3. Role 复制:类似 Policy,全局复制
  4. Binding Rule 和 Auth Method:全局复制

一致性保证

  • 复制基于 Raft Index 递增顺序,保证因果一致性
  • Blocking Query 机制,主 DC 有更新时立即触发复制
  • 复制失败时自动重试,回退指数退避

延迟

  • 正常情况:1-3 秒(取决于网络延迟)
  • 网络分区:复制暂停,恢复后自动追赶(catch-up)

6. 关键功能实现分析

6.1 ACL Resolver - Token 解析与缓存

6.1.1 功能概述

ACL Resolver 是 ACL 模块的核心组件,负责将 Token SecretID 解析为 Authorizer 对象。Resolver 使用 LRU 缓存避免重复解析,显著提升性能。

6.1.2 核心数据结构

type ACLResolver struct {
    config    ACLResolverConfig
    logger    hclog.Logger
    delegate  ACLResolverBackend // State Store 的抽象接口
    aclConf   *acl.Config
    
    // Token 缓存:SecretID -> Authorizer
    cache *lru.TwoQueueCache
    
    // Identity 缓存:AccessorID -> Identity
    identityCache *lru.TwoQueueCache
    
    // Policy 缓存:PolicyID -> ParsedPolicy
    policyCache *lru.TwoQueueCache
    
    // Role 缓存:RoleID -> Role
    roleCache *lru.TwoQueueCache
    
    // Sentinel值:匿名Token、已删除Token等
    sentinel map[string]*authorizerCacheEntry
}

type authorizerCacheEntry struct {
    authorizer  acl.Authorizer
    token       *structs.ACLToken
    expiresAt   time.Time
    fetchedAt   time.Time
}

6.1.3 核心方法实现

ResolveToken - Token 解析入口

func (r *ACLResolver) ResolveToken(secretID string) (acl.Authorizer, error) {
    // 1. 检查是否为匿名 Token
    if secretID == "" || secretID == acl.AnonymousTokenSecret {
        return r.anonymousAuthorizer()
    }
    
    // 2. 查询缓存
    if entry, ok := r.cache.Get(secretID); ok {
        cached := entry.(*authorizerCacheEntry)
        
        // 检查是否过期
        if time.Now().Before(cached.expiresAt) {
            return cached.authorizer, nil
        }
        
        // 缓存过期,移除
        r.cache.Remove(secretID)
    }
    
    // 3. 从 Backend 读取 Token
    token, err := r.delegate.ACLTokenGetBySecret(secretID)
    if err != nil {
        return nil, err
    }
    if token == nil {
        return nil, acl.ErrNotFound
    }
    
    // 4. 检查 Token 是否过期
    if !token.ExpirationTime.IsZero() && time.Now().After(token.ExpirationTime) {
        // 异步删除过期 Token
        go r.delegate.ACLTokenDelete(token.AccessorID)
        return nil, acl.ErrNotFound
    }
    
    // 5. 解析 Token 关联的 Policies 和 Roles
    policies, err := r.resolvePoliciesForToken(token)
    if err != nil {
        return nil, err
    }
    
    // 6. 编译 Policies 为 Authorizer
    authorizer, err := r.newAuthorizerFromPolicies(policies)
    if err != nil {
        return nil, err
    }
    
    // 7. 存入缓存
    entry := &authorizerCacheEntry{
        authorizer: authorizer,
        token:      token,
        expiresAt:  time.Now().Add(r.config.ACLTokenTTL), // 默认 30s
        fetchedAt:  time.Now(),
    }
    r.cache.Add(secretID, entry)
    
    return authorizer, nil
}

resolvePoliciesForToken - 解析 Token 的所有 Policy

func (r *ACLResolver) resolvePoliciesForToken(token *structs.ACLToken) ([]*structs.ACLPolicy, error) {
    var policies []*structs.ACLPolicy
    
    // 1. 直接关联的 Policies
    for _, link := range token.Policies {
        policy, err := r.resolvePolicyByID(link.ID)
        if err != nil {
            return nil, err
        }
        if policy != nil {
            policies = append(policies, policy)
        }
    }
    
    // 2. 通过 Roles 关联的 Policies
    for _, roleLink := range token.Roles {
        role, err := r.resolveRoleByID(roleLink.ID)
        if err != nil {
            return nil, err
        }
        if role == nil {
            continue
        }
        
        for _, policyLink := range role.Policies {
            policy, err := r.resolvePolicyByID(policyLink.ID)
            if err != nil {
                return nil, err
            }
            if policy != nil {
                policies = append(policies, policy)
            }
        }
    }
    
    // 3. ServiceIdentities 转换为隐式 Policy
    for _, si := range token.ServiceIdentities {
        policy := synthesizeServiceIdentityPolicy(si)
        policies = append(policies, policy)
    }
    
    // 4. NodeIdentities 转换为隐式 Policy
    for _, ni := range token.NodeIdentities {
        policy := synthesizeNodeIdentityPolicy(ni)
        policies = append(policies, policy)
    }
    
    return policies, nil
}

synthesizeServiceIdentityPolicy - ServiceIdentity 隐式策略生成

func synthesizeServiceIdentityPolicy(si *structs.ACLServiceIdentity) *structs.ACLPolicy {
    // ServiceIdentity 自动生成以下权限:
    // - service:write (允许注册服务)
    // - service:read (允许查询服务)
    // - node:read (允许查询节点)
    // - intention:read (允许读取意图)
    
    rules := fmt.Sprintf(`
service "%s" {
  policy = "write"
}
service "%s-sidecar-proxy" {
  policy = "write"
}
node_prefix "" {
  policy = "read"
}
`, si.ServiceName, si.ServiceName)
    
    return &structs.ACLPolicy{
        ID:          fmt.Sprintf("synthetic-policy-service-%s", si.ServiceName),
        Name:        fmt.Sprintf("service-identity-%s", si.ServiceName),
        Description: "Synthetic policy generated from service identity",
        Rules:       rules,
    }
}

6.1.4 缓存策略

缓存类型

  • 2Q Cache(Two-Queue LRU):分为频繁访问队列和最近访问队列,提升缓存命中率
  • 容量:Token Cache 默认 1024 条,Policy Cache 512 条,Role Cache 256 条
  • TTL:Token 缓存 30 秒,Policy/Role 缓存 30 秒

缓存失效

  • 时间失效:超过 TTL 自动过期
  • 主动失效:Token/Policy/Role 更新/删除时,通过 cache.Remove() 主动清除
  • Token 过期:检测到 ExpirationTime 已过期,自动剔除

缓存预热

  • Server 启动时,Resolver 不预加载缓存(懒加载)
  • 首次请求触发加载,后续请求命中缓存

6.2 PolicyAuthorizer - 权限判定引擎

6.2.1 功能概述

PolicyAuthorizer 实现 Authorizer 接口,基于编译后的 Policy 规则进行权限判定。判定逻辑为前缀匹配 + 优先级合并。

6.2.2 核心数据结构

type PolicyAuthorizer struct {
    // 默认策略(allow 或 deny)
    defaultPolicy acl.EnforcementDecision
    
    // 各资源的规则树
    serviceRules   *policyRuleSet
    nodeRules      *policyRuleSet
    keyRules       *policyRuleSet
    sessionRules   *policyRuleSet
    eventRules     *policyRuleSet
    queryRules     *policyRuleSet
    
    // 全局权限
    aclRule      string // "read" / "write" / "deny"
    keyringRule  string
    operatorRule string
    meshRule     string
    peeringRule  string
}

type policyRuleSet struct {
    // 精确匹配规则:资源名 -> 策略级别
    exactRules map[string]string
    
    // 前缀匹配规则:前缀 -> 策略级别(按前缀长度排序)
    prefixRules []prefixRule
}

type prefixRule struct {
    prefix string
    policy string
}

6.2.3 核心方法实现

ServiceWrite - 服务写权限判定

func (p *PolicyAuthorizer) ServiceWrite(name string, ctx *acl.AuthorizerContext) acl.EnforcementDecision {
    // 1. 查找精确匹配规则
    if policy, ok := p.serviceRules.exactRules[name]; ok {
        return policyToDecision(policy, "write")
    }
    
    // 2. 查找前缀匹配规则(最长匹配优先)
    for i := len(p.serviceRules.prefixRules) - 1; i >= 0; i-- {
        rule := p.serviceRules.prefixRules[i]
        if strings.HasPrefix(name, rule.prefix) {
            return policyToDecision(rule.policy, "write")
        }
    }
    
    // 3. 无匹配规则,返回 Default(由默认策略决定)
    return acl.Default
}

func policyToDecision(policy string, requiredLevel string) acl.EnforcementDecision {
    levels := map[string]int{
        "deny":  0,
        "read":  1,
        "list":  2,
        "write": 3,
    }
    
    required := levels[requiredLevel]
    actual := levels[policy]
    
    if actual >= required {
        return acl.Allow
    }
    if policy == "deny" {
        return acl.Deny
    }
    return acl.Default
}

newAuthorizerFromPolicies - 从 Policy 列表构建 Authorizer

func (r *ACLResolver) newAuthorizerFromPolicies(policies []*structs.ACLPolicy) (acl.Authorizer, error) {
    var parsedPolicies []*acl.Policy
    
    // 1. 解析所有 Policy 的 Rules(HCL -> 结构化)
    for _, policy := range policies {
        parsed, err := acl.NewPolicyFromSource(policy.Rules, &acl.Config{})
        if err != nil {
            return nil, fmt.Errorf("Failed to parse policy %s: %w", policy.Name, err)
        }
        parsedPolicies = append(parsedPolicies, parsed)
    }
    
    // 2. 合并所有 Policy(Deny 优先)
    merged, err := acl.MergePolicies(parsedPolicies)
    if err != nil {
        return nil, err
    }
    
    // 3. 创建 PolicyAuthorizer
    authorizer, err := acl.NewPolicyAuthorizerWithDefaults(merged, &acl.Config{
        WildcardName:  "*",
        DefaultPolicy: r.config.ACLDefaultPolicy, // "allow" 或 "deny"
    })
    
    return authorizer, err
}

MergePolicies - 策略合并逻辑

func MergePolicies(policies []*Policy) (*Policy, error) {
    merged := &Policy{}
    
    for _, policy := range policies {
        // 1. Service 规则合并
        for _, rule := range policy.Services {
            existing := merged.ServiceRules[rule.Name]
            // Deny > Write > Read > List
            merged.ServiceRules[rule.Name] = maxPolicy(existing, rule.Policy)
        }
        
        // 2. Node 规则合并
        for _, rule := range policy.Nodes {
            existing := merged.NodeRules[rule.Name]
            merged.NodeRules[rule.Name] = maxPolicy(existing, rule.Policy)
        }
        
        // 3. Key 规则合并
        for _, rule := range policy.Keys {
            existing := merged.KeyRules[rule.Prefix]
            merged.KeyRules[rule.Prefix] = maxPolicy(existing, rule.Policy)
        }
        
        // 4. 全局权限合并
        merged.ACL = maxPolicy(merged.ACL, policy.ACL)
        merged.Keyring = maxPolicy(merged.Keyring, policy.Keyring)
        merged.Operator = maxPolicy(merged.Operator, policy.Operator)
    }
    
    return merged, nil
}

func maxPolicy(a, b string) string {
    priority := map[string]int{"deny": 4, "write": 3, "read": 2, "list": 1, "": 0}
    if priority[a] > priority[b] {
        return a
    }
    return b
}

7. 配置与最佳实践

7.1 关键配置项

配置项 默认值 说明 调优建议
acl.enabled false 是否启用 ACL 生产环境必须启用
acl.default_policy allow 默认策略 生产环境设置为 deny
acl.enable_token_persistence true 是否持久化 Agent Token 建议启用,避免重启后丢失
acl.token_ttl 30s Token 缓存 TTL 默认值合理,高频更新可降低到 10s
acl.policy_ttl 30s Policy 缓存 TTL 默认值合理
acl.role_ttl 30s Role 缓存 TTL 默认值合理
acl.down_policy “extend-cache” ACL 系统不可用时的降级策略 “extend-cache” 保持可用性,“deny” 保持安全性

7.2 最佳实践

1. 最小权限原则

  • 避免使用 global-management 策略的 Token 进行日常操作
  • 为每个应用/服务创建独立 Token,仅授予必要权限
  • 使用 ServiceIdentity 简化服务 Token 配置

2. Token 生命周期管理

  • 为应用 Token 设置 ExpirationTTL(如 24 小时),强制定期轮换
  • 使用 Auth Method + Binding Rule 实现自动化 Token 发放
  • 定期审计 Token 列表,删除不再使用的 Token

3. 策略设计

  • 使用 Role 组织 Policy,避免 Token 直接关联大量 Policy
  • Policy 命名规范:<team>-<resource>-<level>(如 platform-service-read
  • 避免过度使用通配符前缀(影响性能)

4. 复制与高可用

  • 主数据中心使用 3 或 5 个 Server,保证 ACL 系统高可用
  • 从数据中心启用 Token 复制(acl.tokens.replication
  • 监控复制延迟(consul.acl.replication.lag

5. 安全加固

  • 启用 TLS 加密所有 RPC/HTTP 通信
  • 限制 Bootstrap 操作(删除 Reset Index 文件)
  • 日志脱敏(SecretID 自动替换为 <redacted>

7.3 故障排查

Token 无效/权限不足

# 1. 验证 Token 是否存在
consul acl token read -id <accessor-id>

# 2. 检查 Token 关联的 Policy
consul acl token read -id <accessor-id> -expanded

# 3. 测试权限
consul acl token test -token <secret-id>

复制延迟过高

# 查看复制状态
consul acl replication-status

# 检查网络延迟(主 DC -> 从 DC)
ping <primary-dc-server>

# 查看 Raft 日志堆积
consul operator raft list-peers

Resolver 缓存失效

# 查看 Resolver 指标
curl http://localhost:8500/v1/agent/metrics | grep acl.resolveToken

# 清空 Resolver 缓存(重启 Server)
consul reload

8. 性能与容量规划

8.1 性能指标

典型延迟(3节点集群,LAN网络)

操作 P50 P95 P99
Token 解析(缓存命中) 0.5ms 1ms 2ms
Token 解析(缓存未命中) 5ms 10ms 20ms
Token 创建 20ms 40ms 80ms
Policy 创建 15ms 30ms 60ms
Login(Auth Method) 30ms 60ms 100ms
权限判定(ServiceWrite) 0.1ms 0.3ms 0.5ms

吞吐量

  • Resolver 解析 QPS:10,000+(缓存命中)
  • Token 创建 TPS:100-200(受 Raft 限制)
  • ACL 写操作 TPS:200-500(批量操作)

8.2 容量上界

资源 推荐上限 硬限制 说明
Token 总数 100,000 受 State Store 内存限制
Policy 总数 10,000 影响 Resolver 初始化时间
单 Token 关联 Policy 数 10 影响解析性能
单 Policy 规则数 1000 影响编译时间
Auth Method 数量 100 -
Binding Rule 数量 1000 Login 时遍历所有规则

8.3 监控告警

关键指标

  • consul.acl.resolveToken - Token 解析延迟
  • consul.acl.cache.hit / consul.acl.cache.miss - 缓存命中率
  • consul.acl.replication.lag - 复制延迟(秒)
  • consul.raft.apply - Raft Apply 延迟(影响 ACL 写操作)

告警阈值

  • Resolver 缓存命中率 < 90%
  • Token 解析 P99 > 50ms
  • 复制延迟 > 10 秒
  • Token/Policy 创建失败率 > 1%