被裁员工删掉 96 个政府数据库——离岗访问权为什么必须秒级收回

2026-05-14 27 预计阅读时间:1 分钟
来源:oschina.net AI 摘要 原文链接

免责声明:本文为 AI 摘要整理,建议结合原文阅读。摘要可能省略上下文、版本差异或边界条件,不作为官方说明。

预计阅读时间:11 分钟

双胞胎兄弟阿赫特尔在被裁后,利用尚未注销的系统权限删掉了 96 个政府数据库。这不是电影情节,是真实发生的事件。它暴露了一个很多组织仍在拖延的问题:员工离岗的那一刻,数字凭证必须立刻失效。

"登录失败"才是最诚实的裁员通知

美国科技行业有个冷惯例——员工发现自己被裁,往往不是因为 HR 的邮件,而是因为 Slack 登不上了、VPN 连不上了、GitHub token 报 401 了。做法冷酷,逻辑却硬:一个已经知道要离开但仍保有系统访问权的人,是最大的内部威胁之一。

阿赫特尔兄弟的案例把这条逻辑验证到了极端:他们不仅还有权限,而且权限大到能批量删除政府数据库。这说明涉事机构在离岗流程上至少犯了两个错误:

  • 凭证回收滞后,裁员决定与权限注销之间存在时间窗口;
  • 权限过度授予,日常运维需要的能力与批量删除数据库的能力之间没有隔离。

内部威胁的真实成本

外部攻击需要突破防线,内部攻击者已经在防线之内。 Verizon 的数据泄露报告反复指出,内部人员造成的泄露事件虽然占比不如外部攻击,但单次事件的平均损失更高——因为他们知道数据在哪、系统怎么运作、审计盲区在哪。

阿赫特尔删掉 96 个数据库,恢复成本远不止技术层面:政府服务中断、公众信任受损、合规调查与法律追诉,每一项都是长期负担。

离岗权限回收的工程实践

把"立刻注销凭证"从口号变成工程动作,需要自动化流水线和分层权限设计。下面给出几个可直接落地或改造的示例。

1. 用 IAM 条件策略实现"入职即授权、离岗即失效"

AWS IAM 支持基于时间的条件键,可以给临时员工或合同工设置访问窗口,到期自动失效——不依赖人工操作。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:ListBucket"],
      "Resource": ["arn:aws:s3:::gov-data-bucket", "arn:aws:s3:::gov-data-bucket/*"],
      "Condition": {
        "DateLessThan": {
          "aws:CurrentTime": "2025-08-01T00:00:00Z"
        },
        "DateGreaterThan": {
          "aws:CurrentTime": "2025-01-15T00:00:00Z"
        }
      }
    }
  ]
}

策略到期后,所有匹配的 API 调用自动返回 AccessDenied,无需人工干预。注意:此策略仅限制 API 调用,已生成的临时 session token 如果仍在有效期内仍可使用,因此必须配合较短的 session duration(如 1 小时)。

2. 离岗事件触发的自动化凭证回收脚本

以下 Python 脚本模拟一个 HR 系统发出离岗事件后,自动调用各平台 API 注销凭证的流程。实际部署时需要替换为真实的 API endpoint 和认证方式。

"""
offboard_revoke.py — 离岗时自动回收数字凭证
运行前设置环境变量:
  export SLACK_ADMIN_TOKEN=xoxp-xxx
  export GITHUB_ADMIN_TOKEN=ghp_xxx
  export AWS_PROFILE=gov-admin
  export DB_ADMIN_CONN="postgresql://admin:pass@db-host:5432/govdb"
"""

import os
import sys
import boto3
import requests
from datetime import datetime

def revoke_aws_access(username: str):
    """删除 IAM 用户,挂起所有关联 access key"""
    iam = boto3.client("iam")
    # 列出并删除所有 access key
    keys = iam.list_access_keys(UserName=username)
    for key in keys["AccessKeyMetadata"]:
        iam.delete_access_key(UserName=username, AccessKeyId=key["AccessKeyId"])
        print(f"[AWS] 已删除 access key: {key['AccessKeyId']}")
    # 删除用户
    iam.delete_user(UserName=username)
    print(f"[AWS] 已删除 IAM 用户: {username}")

def revoke_slack_access(email: str):
    """禁用 Slack 账号"""
    token = os.environ["SLACK_ADMIN_TOKEN"]
    # 先查找用户 ID
    resp = requests.get(
        "https://slack.com/api/users.lookupByEmail",
        headers={"Authorization": f"Bearer {token}"},
        params={"email": email},
    )
    user_id = resp.json()["user"]["id"]
    # 禁用账号
    requests.post(
        "https://slack.com/api/admin.users.disable",
        headers={"Authorization": f"Bearer {token}"},
        json={"user_id": user_id},
    )
    print(f"[Slack] 已禁用账号: {email}")

def revoke_db_access(username: str):
    """撤销数据库角色权限并删除用户"""
    import psycopg
    conn_str = os.environ["DB_ADMIN_CONN"]
    with psycopg.connect(conn_str) as conn:
        with conn.cursor() as cur:
            cur.execute(f"REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA public FROM {username};")
            cur.execute(f"REVOKE ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public FROM {username};")
            cur.execute(f"DROP ROLE IF EXISTS {username};")
            print(f"[DB] 已撤销权限并删除角色: {username}")

def offboard(employee_id: str, username: str, email: str):
    """离岗流水线:依次回收各系统凭证"""
    print(f"=== 离岗凭证回收开始 {datetime.now()} ===")
    print(f"员工: {employee_id} / {username} / {email}")
    try:
        revoke_aws_access(username)
        revoke_slack_access(email)
        revoke_db_access(username)
    except Exception as e:
        print(f"[ERROR] 凭证回收失败: {e}")
        sys.exit(1)
    print(f"=== 离岗凭证回收完成 {datetime.now()} ===")

if __name__ == "__main__":
    # 实际部署中,这些参数应来自 HR 事件消息(如 Kafka topic / webhook)
    offboard(
        employee_id=sys.argv[1],
        username=sys.argv[2],
        email=sys.argv[3],
    )

运行方式:

python offboard_revoke.py EMP-00421 akhter akhter@gov.example.com

关键设计点: 脚本应该由离岗事件自动触发(HR 系统 webhook、Kafka 消息、EventBridge 规则),而不是靠运维人员手动执行。手动执行就有时间窗口,有时间窗口就有风险。

3. 用 Kubernetes RBAC + 审计日志防止"删库级"权限滥用

如果数据库运维在 Kubernetes 集群内进行,可以通过 RBAC 限制谁能执行删除操作,并强制所有操作写入审计日志。

# 只允许数据运维组执行读取和备份,禁止删除
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: gov-data
  name: data-ops-reader
rules:
  - apiGroups: [""]
    resources: ["pods", "pods/log"]
    verbs: ["get", "list"]
  - apiGroups: ["batch"]
    resources: ["jobs"]
    verbs: ["get", "list", "create"]  # 允许创建备份 job,但不允许删除
---
# 删除操作需要单独的高权限角色,且绑定到极少数人
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: gov-data
  name: data-ops-admin
rules:
  - apiGroups: [""]
    resources: ["pods"]
    verbs: ["delete"]  # 仅此角色可删 pod
  - apiGroups: ["batch"]
    resources: ["jobs"]
    verbs: ["delete"]

同时在 apiserver 启动参数中启用审计:

--audit-log-path=/var/log/kubernetes/audit.log
--audit-log-maxage=30
--audit-policy-file=/etc/kubernetes/audit-policy.yaml

审计策略中,对 delete 操作记录完整请求体:

apiVersion: audit.k8s.io/v1
kind: Policy
rules:
  - level: RequestResponse
    verbs: ["delete", "deletecollection"]
    resources:
      - group: ""
        resources: ["pods", "secrets", "configmaps"]
      - group: "batch"
        resources: ["jobs", "cronjobs"]

这样即使有人拿到了删除权限,每一次删除操作都会被完整记录,事后可追溯。

权限设计的三个硬原则

从阿赫特尔事件中可以提炼出三条原则,适用于任何管理敏感数据的组织:

原则 含义 反面案例
最小权限 只授予完成当前任务所需的最低权限 日常运维人员能批量 DROP DATABASE
即时失效 离岗决定生效的同时,所有凭证必须失效 被裁后数小时甚至数天仍可登录
分层隔离 读取、写入、删除权限分别授予不同角色/层级 一个角色同时拥有读、写、删全部权限

这三条原则互相支撑:最小权限降低了即时失效的执行范围(要回收的凭证更少),分层隔离让即时失效可以分批执行(先收删除权限,再收读取权限),即时失效又为分层隔离提供了时间保障(高权限角色离岗时立刻被剥离)。

离岗安全检查清单

每次员工离岗(裁员、辞职、合同到期、调岗),运维团队应逐项确认:

  • ☐ 所有 IAM / RBAC 角色绑定已移除
  • ☐ 所有 access key / API token 已删除或吊销
  • ☐ VPN / SSH / 数据库直连凭证已注销
  • ☐ Slack / GitHub / Jira 等协作平台账号已禁用
  • ☐ 离岗事件已触发自动化回收流水线(而非依赖手动操作)
  • ☐ 审计日志已确认该用户最后一条活动记录的时间戳在回收动作之前
  • ☐ 如该用户持有删除级权限,回收动作必须在裁员通知发出之前完成

最后一条听起来冷酷,但阿赫特尔兄弟用 96 个数据库的代价证明了:安全回收凭证的时机不是"员工离开办公室之后",而是"员工知道自己要离开之前"。


相关推荐