亚马逊AWS官方博客
借助 Cloud Foundations 从零开始一键部署配置容器化 KeyCloak 用户联合验证方案及配套数据库共享网络等依赖资源
Cloud Foundations 解决方案通过资源即代码和自动化运维技术快速构建多账户组织云上运行环境,类似通常所说的“着陆区”或“云底座”。除着陆区构建以外,Cloud Foundations 通过 Amazon Service Catalog 服务目录产品形式提供各种各样的云基础设施相关功能产品,例如构建跨区域多账户共享网络[1],云资源定义部署自动化管理[2],不合规安全配置检测与修正,用户联合验证等等。本文就其中的用户联合验证产品部署配置过程进行介绍,并与目前其他类似解决方案进行比较讨论。
一般而言,基于 Amazon Organizations 原生组织部署时,最佳实践是开启 Amazon IAM Identity Center 服务集中管理对多账户和应用程序的访问权限。如果需要在该集中管理范围之外另设一套体系管理员工访问权限,又或者不是基于原生组织而是虚拟组织部署时,可以考虑采用 Cloud Foundations 标准包包含的基于 KeyCloak 的用户联合验证产品进行多账户集中访问权限管理。前述适用场景来自真实客户需求,且实际部署验证过。读者可在此基础上根据自身实际情况做进一步优化调整。
总体架构
Cloud Foundations 集成的 KeyCloak 方案除了 KeyCloak 自带的特性以外主要提供用户联合验证功能,可以将系统预置角色配置至 KeyCloak 的域(realm)中,达到“开箱即用”的效果。下图展示方案总体架构,其中:
- 通过 Cloud Foundations 共享网络产品构建网络架构,分别共享至安全、基础账户,从网络地址转换 VPC 统一互联网出口。读者亦可构建其他符合实际需求的网络架构,例如共享中转网关;
 - 主机名称方面利用 Amazon Route 53 的私有托管区绑定安全账户和网络地址转换 VPC,添加 A 记录关联安全账户的负载均衡器 DNS 名称;
 - 通过 Cloud Foundations 产品工厂定义 KeyCloak 所需云基础设施资源并通过流水线部署于安全账户,通过专门客户托管密钥对相关资源统一加密;
 - 通过 Cloud Foundations 流水线工厂执行 Amazon CodeBuild 项目,完成如下主要操作:通过自动化文档拉取最新版 KeyCloak 官方镜像并推送至安全账户 Amazon ECR 镜像库;通过 KeyCloak 应用程序接口配置 Cloud Foundations 域和预置角色、用户;获取 SAML 配置文件并保存;
 - 通过 Cloud Foundations 流水线工厂执行 Amazon CodeBuild 项目,在相关核心账户创建安全断言标记语言 (SAML) 身份提供者和创建用于 SAML 联合身份验证的预置角色实现联合单点登录。如无需部署预置角色则此步可略过;
 
![]()  |  
         
类似方案
我们注意到其他在亚马逊云科技上部署 KeyCloak 的解决方案,在此就十大主要评估点进行比较,方便读者取舍。
| 要点 | Cloud Foundations 的 KeyCloak 方案 | 基于 KeyCloak 的身份及访问控制系统[3] | 
| KeyCloak 版本 | 最新版(目前25.0.4) | 限定版本 22.0.4 | 
| 镜像位置 | 优化后置于安全账户 ECR 镜像库 | 官网直接获取或第三方账户 | 
| 数据库 | Amazon RDS PostgreSQL 或指定其他 RDS 数据库 | 仅 Amazon Aurora Serverless 数据库 | 
| 网络环境 | 通过共享网络产品定义并一键部署复杂网络 | 自行准备或创建固定 CIDR 的简单新 VPC | 
| KeyCloak 基础设施资源 | 通过产品工厂自动完成 | 通过 CloudFormation 模版 | 
| 定制 KeyCloak 基础设施资源 | 修改 KeyCloak 基础设施产品定义 | 无 | 
| 创建 Route 53 记录以解析域名 | 自动 | 手动 | 
| 数据库密码管理 | 集成密码管理器管理主用户密码 | 自行管理主用户密码 | 
| 加密对象 | Amazon ECS 集群,Amazon Fargate 临时存储,ECR 镜像库,RDS 数据库(及其性能洞察、主用户密码),Amazon CloudWatch 日志组 | 无 | 
| 配置 KeyCloak 域、组、角色和用户 | 通过流水线工厂自动完成 | 无 | 
| 配置 SAML 身份提供者及预置 IAM 角色 | 通过流水线工厂自动完成 | 无 | 
| 支持国内外区域 | 是 | 是 | 
从上表可以看出,本方案采用更新的 KeyCloak 版本,通过本地镜像库达到更快的读取速度,云资源产品可以灵活自定义更改,对云资源可以加密的都加密,通过自动化技术完成大部分创建配置操作。读者可根据上述特性结合自身情况作出合适选择。
实施步骤
从总体架构的介绍不难看出,本方案分以下几大步依次完成实施部署,根据熟练程度,总耗时约一至二小时:
- 准备网络环境;
 - 准备证书;
 - 部署 KeyCloak 基础设施资源;
 - 部署 KeyCloak 镜像并配置域、组、角色和用户;
 - 配置 SAML 身份提供者和联合身份验证预置角色(可选);
 
1. 准备网络环境
网络架构参考前述总体架构网络部分。以下示例代码只是其中一种集中互联网出口的拓扑架构,读者可根据自身实际需要构建其他网络环境并利用网络防火墙检查流量等。对本方案而言只需要满足以下条件即可:
- 共享至安全账户的 VPC:包含 3 个私有子网且可访问互联网;
 - 共享至基础账户的 VPC:包含 1 个私有子网且可访问安全账户,名称为 infra; 
{ "vpcs": { "hub": { "cidr": "192.168.0.0/20", "is_hub": true, "enable_igw": true, "enable_nat": true, "subnets": [ [[8, 0], [8, 1]], [[4, 1], [4, 2]] ] }, "infra": { "cidr": "10.0.16.0/20", "accounts": ["$.account.infra"], "gw_endpoints": ["s3"], "subnets": [ [[8, 0], [8, 1]], [], [[4, 1], [4, 2]] ] }, "security": { "cidr": "10.0.32.0/20", "accounts": ["$.account.security"], "gw_endpoints": ["s3"], "subnets": [ [[8, 0], [8, 1]], [], [[4, 1], [4, 2]], [[4, 3], [4, 4]], [[4, 5], [4, 6]] ] } }, "tgw": { "enabled": true, "cidr": "10.0.0.0/16", "tables": { "pre": { "associations": ["infra", "security"], "routes": { "*": "hub" } }, "post": { "associations": ["hub"], "propagations": ["infra", "security"] } } } } 
部署网络环境的主要步骤为:
- 在基础账户 cloud-foundations 应用中定义默认阶段网络结构并保存到 network-vpc 配置文件中。该配置文件名称是固定的;
 - 在基础账户中启动流水线工厂服务目录产品,路径为 network/vpc,选择单账户模式,填入网络账户,区域、阶段、变量均留空。此时部署于主区域,生成一条流水线;
 - 执行并批准网络流水线 pipeline-network-vpc-apply,部署默认阶段网络资源至主区域;
 
2. 准备证书
本方案采用自签名。如读者有公有 SSL/TLS 证书,导入 Amazon Certificate Manager 证书管理器后直接替换使用即可。系统通过指定标签键值对读取待使用的证书。以下为生成和导入证书至安全账户,上传密钥文件至日志账户的命令:
- 生成证书,域为公司域名,例如 company.com: 
openssl req -nodes -x509 -newkey rsa:2048 -days 365 \ -keyout PrivateKey.pem \ -out Certificate.pem - 导入证书至安全账户证书管理器: 
aws acm import-certificate \ --certificate fileb://Certificate.pem \ --private-key fileb://PrivateKey.pem \ --tags Key=Name,Value=前缀-certificate-keycloak - 上传证书至日志账户 terraform 桶证书目录下: 
aws s3 sync . s3://前缀-bucket-logs-terraform-日志账户/terraform/keycloak/ \ --sse aws:kms \ --sse-kms-key-id $TERRAFORM_KEY_ARN 
3. 部署 KeyCloak 基础设施资源
本方案通过 Cloud Foundations 产品工厂定义并部署运行 KeyCloak 所需的基础设施资源,如前述总体架构图安全账户方框内所示。由于资源之间相互依赖关系,产品共分五层依次部署配置。整体结构如下所示:
![]()  |  
         
该产品预设以下环境变量以便读者根据实际情况调整:
| 属性 | 示例 | 内容 | 
| CommonCidr | 10.0.0.0/16 | 公共网段,通常为中转网关网段 | 
| Domain | company.com | 导入 ACM 的证书域 | 
| KeyCloakName | keycloak | KeyCloak 名称 | 
| NetworkAccount | 123456789012 | 网络账户 | 
| Route53Enabled | true | 开启 Route53 支持 | 
| RdsMultiAz | true | 开启多可用区部署 | 
| RdsStorageSize | 30 | 数据库存储空间 | 
| RdsStorageMax | 100 | 数据库存储最大空间,0 则不扩容 | 
| RdsTimeZone | Asia/Shanghai | 数据库时区 | 
| RdsType | db.m5d.large | 数据库实例类型 | 
以下为产品定义 JSON 代码,部署 Cloud Foundations 时系统预置于基础账户的 product 应用中并命名为 cf-keycloak-infra,读者可以直接使用或根据实际情况调整保存后使用。
[
  [
    {
      "service": "kms",
      "accounts": ["$.account.security"],
      "keys": { "${KeyCloakName}": { "allows": ["self", "fargate", "logs"] } }
    },
    {
      "service": "ecs",
      "accounts": ["$.account.security"],
      "settings": { "${KeyCloakName}": { "enables": ["containerInsights"] } }
    },
    {
      "service": "rds",
      "accounts": ["$.account.security"],
      "settings": { "${KeyCloakName}": { "monitor_role": true } },
      "groups": { "${KeyCloakName}": { "subnets": "$.private-main-security-sn2" } }
    }
  ],
  [
    {
      "service": "ecr",
      "accounts": ["$.account.security"],
      "instances": { "${KeyCloakName}": { "key": "$.${KeyCloakName}" } }
    },
    {
      "service": "ecs",
      "accounts": ["$.account.security"],
      "instances": {
        "${KeyCloakName}": {
          "key": "$.${KeyCloakName}",
          "key_fargate": "$.${KeyCloakName}",
          "key_log": "$.${KeyCloakName}"
        }
      }
    },
    {
      "service": "secretsmanager",
      "accounts": ["$.account.security"],
      "secrets": {
        "${KeyCloakName}-ecs": {
          "key": "$.${KeyCloakName}",
          "json": { "username": "${KeyCloakName}-admin", "password": "$${PASSWORD}" }
        }
      }
    },
    {
      "service": "logs",
      "accounts": ["$.account.security"],
      "groups": { "ecs/task/${KeyCloakName}": {} }
    },
    {
      "service": "logs",
      "accounts": ["$.account.infra"],
      "groups": { "ssm/security/${KeyCloakName}/docker": {} }
    }
  ],
  [
    {
      "service": "iam",
      "accounts": ["$.account.security"],
      "roles": {
        "${KeyCloakName}-exec": {
          "trusts": ["ecs-tasks"],
          "aws_policies": ["AmazonECSTaskExecutionRolePolicy"],
          "statements": [
            { "actions": ["kms:Decrypt"], "resources": ["key:$.${KeyCloakName}"] },
            {
              "actions": ["secretsmanager:GetSecretValue"],
              "resources": ["secret:$.${KeyCloakName}-ecs"]
            },
            { "actions": ["logs:CreateLogGroup"] }
          ]
        },
        "${KeyCloakName}-task": {
          "trusts": ["ecs-tasks"],
          "statements": [
            {
              "actions": [
                "ssmmessages:CreateControlChannel",
                "ssmmessages:CreateDataChannel",
                "ssmmessages:OpenControlChannel",
                "ssmmessages:OpenDataChannel"
              ]
            }
          ]
        }
      }
    },
    {
      "service": "iam",
      "accounts": ["$.account.infra"],
      "roles": {
        "${KeyCloakName}-ec2": {
          "trusts": ["ec2"],
          "aws_policies": ["CloudWatchAgentServerPolicy", "AmazonSSMManagedInstanceCore"],
          "statements": [
            {
              "actions": ["sts:AssumeRole"],
              "resources": ["arn:${PARTITION}:iam::${SECURITY_ACCOUNT}:role/cf-manager-role"]
            },
            {
              "actions": [
                "kms:Decrypt",
                "kms:DescribeKey",
                "kms:GenerateDataKey*"
              ],
              "resources": ["$.key.terraform"]
            },
            {
              "actions": ["s3:GetObject*"],
              "resources": ["$.bucket.terraform/terraform/keycloak/*"]
            }
          ]
        }
      }
    },
    {
      "service": "vpc",
      "accounts": ["$.account.security"],
      "groups": {
        "${KeyCloakName}-lb": {
          "vpc": "$.security",
          "in": [
            { "protocol": "http", "cidr": "*" },
            { "protocol": "https", "cidr": "*" }
          ],
          "out": [{ "protocol": "*", "cidr": "*" }]
        },
        "${KeyCloakName}-ecs": {
          "vpc": "$.security",
          "in": [
            { "cidr": "${CommonCidr}", "ports": [8080, 8080] },
            { "cidr": "${CommonCidr}", "ports": [8443, 8443] },
            { "cidr": "${CommonCidr}", "ports": [9000, 9000] }
          ],
          "out": [{ "protocol": "*", "cidr": "*" }]
        },
        "${KeyCloakName}-rds": {
          "vpc": "$.security",
          "in": [{ "protocol": "postgres", "cidr": "vpc" }]
        }
      }
    },
    {
      "service": "vpc",
      "accounts": ["$.account.infra"],
      "groups": {
        "infra-default": {
          "vpc": "$.infra",
          "out": [{ "protocol": "*", "cidr": "*" }]
        }
      }
    }
  ],
  [
    {
      "service": "rds",
      "accounts": ["$.account.security"],
      "instances": {
        "${KeyCloakName}": {
          "db_name": "${KeyCloakName}",
          "type": "${RdsType}",
          "multi_az": "${RdsMultiAz}",
          "key": "$.${KeyCloakName}",
          "key_insights": "$.${KeyCloakName}",
          "key_secret": "$.${KeyCloakName}",
          "volume": { "size": "${RdsStorageSize}", "max": "${RdsStorageMax}" },
          "parameters": { "timezone": "${RdsTimeZone}" },
          "group": "$.${KeyCloakName}",
          "vpc": { "vpc": "$.security", "groups": "$.${KeyCloakName}-rds" }
        }
      }
    },
    {
      "service": "lb",
      "accounts": ["$.account.security"],
      "groups": {
        "${KeyCloakName}": {
          "health": {
            "matcher": "200-299",
            "path": "/health",
            "port": 9000,
            "protocol": "HTTP"
          },
          "stickiness": { "enabled": true, "type": "lb_cookie", "duration": 3600 },
          "vpc": "$.security",
          "port": 8443,
          "protocol": { "name": "HTTPS" },
          "type": "ip",
          "warm": 60
        }
      },
      "instances": {
        "${KeyCloakName}": {
          "vpc": {
            "vpc": "$.security",
            "subnets": "$.private-main-security-sn0",
            "groups": "$.${KeyCloakName}-lb"
          }
        }
      },
      "listeners": {
        "${KeyCloakName}-http": {
          "instance": "${KeyCloakName}",
          "actions": [
            {
              "type": "redirect",
              "protocol": "HTTPS",
              "port": 443,
              "code": "HTTP_301"
            }
          ]
        },
        "${KeyCloakName}-https": {
          "instance": "${KeyCloakName}",
          "domain": "${Domain}",
          "protocol": "HTTPS",
          "port": 443,
          "actions": [{ "group": "${KeyCloakName}" }]
        }
      }
    }
  ],
  [
    {
      "service": "route53",
      "accounts": ["${NetworkAccount}"],
      "zones": {
        "${REG}.cf.local": { "enabled": "${Route53Enabled}", "vpc": "$.nat", "vpcs": ["$.security"] }
      }
    }
  ]
]
 
       部署基础设施产品的主要步骤为:
- (可选)在基础账户 product 应用中调整修改名为 cf-keycloak-infra 的产品应用配置文件;
 - 启动该产品,产品名称和配置文件都是 cf-keycloak-infra,阶段留空,变量为 JSON 格式的前表键值对;
 - 执行并批准基础设施产品流水线 pipeline-cf-keycloak-infra-apply-fresh,部署基础设施资源至安全和基础账户。其中部署数据库实例耗时较长,约需 10 分钟左右;
 
4. 部署 KeyCloak 镜像并配置域、组、角色和用户
本方案通过 Cloud Foundations 流水线工厂部署用户联合验证产品,如前述总体架构图基础账户方框内所示。该产品从 KeyCloak 官方库拉取最新版镜像,优化编译后上传至安全账户容器镜像库,配置 KeyCloak 的 Cloud Foundations 域、组、角色和用户等。该产品预设以下变量便于读者对集群进行调整:
| 属性 | 示例 | 内容 | 
| Route53Enabled | true | 开启 Route53 支持 | 
| Desired | 2 | 期望任务数量 | 
| CPU | 2 | 中央处理器频率(单位 GB) | 
| Memory | 8 | 内存容量(单位 GB) | 
部署用户联合验证产品的主要步骤为:
- 在基础账户中启动流水线工厂服务目录产品,路径为 security/keycloak,选择已知账户模式,账户、区域、阶段均留空。变量为上表内容 JSON 格式键值对;
 - 执行并批准用户联合验证流水线 pipeline-security-keycloak-apply,配置 KeyCloak 基础设施资源。配置耗时取决于网络快慢;
 
完成此步后,即可访问 KeyCloak 的管理控制台,地址为 http://keycloak.REG.cf.local ,其中 REG 是区域短名称,例如北京是 cnn1,宁夏是 cnnw1。登陆凭证保存在安全账户的密码管理器中。
![]()  |  
         
在左上角选择 Cloud Foundations 域,选择客户端(Clients),找到用户联合验证产品配置的 SAML 客户端。其中客户端标识显示为亚马逊云科技北京区域。
![]()  |  
         
5. 配置 SAML 身份提供者和联合身份验证预置角色
本方案通过 Cloud Foundations 流水线工厂部署用户联合验证角色产品,读者可根据需要选配。部署后,该产品在各核心账户部署和 KeyCloak 互相信任的 SAML 身份提供者以及用于 SAML 联合身份验证的预置 IAM 角色。部署用户联合验证角色产品的主要步骤为:
- 在基础账户中启动流水线工厂服务目录产品,路径为 security/keycloak/roles,选择已知账户模式,账户、区域、阶段、变量均留空;
 - 执行并批准用户联合验证角色流水线 pipeline-security-keycloak-roles-apply,配置联合身份验证的预置 IAM 角色;
 
完成此步后,即可访问 KeyCloak 的用户联合验证地址进行验证并跳转至亚马逊云科技控制台。地产是 https://keycloak.REG.cf.local/realms/cloudfoundations/protocol/saml/clients/aws-cn 。对于全球区来说则以 aws 结尾。系统预置了以下 KeyCloak 用户,分别绑定各自账户的所有预置角色。此安排不一定满足读者要求。您可根据实际情况灵活新建组、角色、用户并调整和角色的绑定情况,具体配置说明可以参考 KeyCloak 官方文档。
- infrastructure-operator;
 - logs-operator;
 - network-operator;
 - security-operator;
 
![]()  |  
         
以安全操作员成功登陆 KeyCloak 后,即跳转至亚马逊云科技控制台,并显示所有和该用户绑定的联合验证预置角色:
![]()  |  
         
销毁资源
销毁资源以节约成本,顺序为创建方案的逆序,主要包含以下步骤:
- 执行并批准用户联合验证角色流水线 pipeline-security-keycloak-roles-destroy;
 - 执行并批准用户联合验证流水线 pipeline-security-keycloak-destroy;
 - 执行并批准基础设施产品流水线 pipeline-cf-keycloak-infra-destroy;
 - 删除自签名证书;
 - 执行并批准网络流水线 pipeline-network-vpc-destroy;
 - 终止用户联合验证角色流水线工厂产品;
 - 终止用户联合验证流水线工厂产品;
 - 终止方案基础设施产品工厂产品;
 - 终止共享网络流水线工厂产品并删除网络定义配置文件;
 
工作展望
本方案从无到有构建基于 KeyCloak 的用户联合验证单点登录方案,从最佳实践到安全规范尽量周全设计,旨在抛砖引玉,为读者构建更契合的联合验证方案打基础。以本方案为出发点,要达到更高标准的生产应用,还有许多工作可以展开落实。可以在公有托管域配置负载均衡器的 DNS 名称使得公网可以访问(中国区需 ICP 备案)。可以配置使用公有证书。可以选择更合适的数据库类型、容量、多可用区部署。可以配置更合适的集群任务期望值、最大最小值。可以配置支持 OIDC 验证。此外还可以从 KeyCloak 与 IAM Identity Center 集成的角度进行拓展。
总结
本文通过具体实践,按五大步骤展示了如何借助 Cloud Foundations 的共享网络,产品工厂,流水线工厂等功能,从零开始构建一套安全完整的 KeyCloak 用户联合验证解决方案,并验证登陆亚马逊云科技控制台。和现有解决方案相较而言,本方案具有许多新特性和灵活度,包括资源处处加密,产品参数可定制可扩展,配置自动化程度高等。读者可在此基础上根据实际情况做进一步优化调整,或者联系亚马逊云科技专家共同打造更契合实际的用户联合验证解决方案。
参考资料
- 博客:《借助 Cloud Foundations 规划设计云上多区域网络轴辐拓扑结构一键部署东西南北流量分别或合并检查》,2023 年 11 月
 - 博客:《借助 Cloud Foundations 产品工厂规划设计并一键部署云上多账户访问控制及权限策略等基础设施资源》,2024 年 3 月
 - 解决方案:《基于Keycloak的身份及访问控制系统》,2024 年 1 月
 





