【DevOps云实践】IaaC:在AWS Application Load Balancer上实现Azure AD的OIDC SSO认证
@TOC
推荐超级课程:
使用AWS应用负载均衡器ALB和Azure AD OIDC简化SSO AWS应用负载均衡器支持一个我认为被低估的功能:在第7层进行请求认证(通过OIDC)。 这使开发人员可以将几乎所有认证都保留在应用层代码之外。一个理想的用例可能是一个仅内部使用的Web应用程序,需要认证,但几乎不需要RBAC授权。
ALB认证通过在监听器规则中定义认证操作来进行。ALB的认证操作将检查传入请求中是否存在会话cookie,然后检查其是否有效。如果会话cookie已设置且有效,则ALB将将请求路由到设置了X-AMZN-OIDC-*
头部的目标组。
ALB认证支持Cognito和通用的OIDC身份提供者。 对于本文,我将重点放在与Azure AD的OIDC 集成上。 并使用Terraform和Serverless Framework进行管理。
[
Azure AD - 企业应用程序配置
为了在Azure AD中设置与OIDC的集成,您首先需要配置一个企业应用程序。微软已经在Azure门户界面中通过手动方式如何做这个的教程,因此我的重点将放在使用Azure AD Terraform提供程序进行部署。这种方法的额外好处是自动提供ALB认证配置输入(用于Serverless)使用生成的Terraform输出。
让我们从Azure应用程序的配置开始:
data "azuread_application_published_app_ids" "well_known" {}
resource "azuread_service_principal" "msgraph" {
application_id = data.azuread_application_published_app_ids.well_known.result.MicrosoftGraph
use_existing = true
}
resource "azuread_application" "app" {
display_name = local.app_title
group_membership_claims = ["SecurityGroup"]
sign_in_audience = "AzureADMyOrg"
web {
homepage_url = local.base_url
redirect_uris = [local.reply_url]
implicit_grant {
access_token_issuance_enabled = false
id_token_issuance_enabled = false
}
}
required_resource_access {
resource_app_id = azuread_service_principal.msgraph.application_id
resource_access {
id = azuread_service_principal.msgraph.oauth2_permission_scope_ids["openid"]
type = "Scope"
}
}
}
resource "azuread_service_principal" "service_principal" {
application_id = azuread_application.app.application_id
tags = ["WindowsAzureActiveDirectoryIntegratedApp"]
app_role_assignment_required = true
}
resource "azuread_application_password" "app_password" {
application_object_id = azuread_application.app.id
display_name = local.app_title
}
resource "azuread_service_principal_delegated_permission_grant" "delegated_grant" {
service_principal_object_id = azuread_service_principal.service_principal.object_id
resource_service_principal_object_id = azuread_service_principal.msgraph.object_id
claim_values = ["openid"]
}
上面的Terraform代码中有两件事情需要注意。
一,我们设置了app_role_assignment_required
,这将要求特定用户在Azure中分配应用程序才能成功进行SSO。
二,azuread_service_principal_delegated_permission_grant
资源会代表企业授予对openid权限的管理员同意。
在Terraform中,我们将利用SSM将生成的应用程序配置参数(用于ALB侦听器)暴露给AWS。
resource "aws_ssm_parameter" "tenant_id" {
name = "/web-app/AZURE_TENANT_ID"
type = "String"
value = data.azuread_client_config.current.tenant_id
}
resource "aws_ssm_parameter" "client_id" {
name = "/web-app/AZURE_CLIENT_ID"
type = "String"
value = azuread_application.app.application_id
}
resource "aws_ssm_parameter" "client_secret" {
name = "/web-app/AZURE_CLIENT_SECRET"
type = "SecureString"
value = azuread_application_password.app_password.value
}
Serverless – ALB Listener
有了创建的Azure企业应用程序,我们可以继续配置ALB本身。让我们先定义这些资源:
Resources:
############
# ALB Resources
############
ALBSecurityGroupEgress:
Type: AWS::EC2::SecurityGroupEgress
Properties:
CidrIp: '0.0.0.0/0' # OIDC needs external egress
GroupId: !GetAtt ALBSecurityGroup.GroupId
IpProtocol: tcp
FromPort: 443
ToPort: 443
ALBSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: ${self:service}-${self:provider.stage}-alb-sg
VpcId: ${self:custom.alb.vpc}
ALBSecurityGroupIngress:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !GetAtt ALBSecurityGroup.GroupId
IpProtocol: tcp
FromPort: ${self:custom.alb.port}
ToPort: ${self:custom.alb.port}
CidrIp: ${self:custom.alb.ingress}
LambdaFunctionPermission:
Type: AWS::Lambda::Permission
Properties:
Action: lambda:InvokeFunction
FunctionName: !GetAtt ApiLambdaFunction.Arn # Dynamic from function
Principal: elasticloadbalancing.amazonaws.com
ALBElasticLoadBalancer:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
Subnets: ${self:custom.alb.subnets}
Scheme: ${self:custom.alb.scheme}
SecurityGroups:
- !GetAtt ALBSecurityGroup.GroupId
DependsOn:
- ALBSecurityGroup
ALBDNSRecord:
Type: AWS::Route53::RecordSet
Properties:
HostedZoneId: ${self:custom.alb.hostedZoneId}
Name: ${self:custom.alb.dnsName}.
AliasTarget:
DNSName: !GetAtt ALBElasticLoadBalancer.DNSName
HostedZoneId: !GetAtt ALBElasticLoadBalancer.CanonicalHostedZoneID
Type: A
LoadBalancerListener:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
Certificates:
- CertificateArn: ${self:custom.alb.certArn}
DefaultActions:
- Type: fixed-response
FixedResponseConfig:
ContentType: "text/plain"
MessageBody: ""
StatusCode: "404"
LoadBalancerArn:
Ref: ALBElasticLoadBalancer
Port: ${self:custom.alb.port}
Protocol: ${self:custom.alb.protocol}
请注意ALB SecurityGroupEgress资源,表示ALB需要外部出口(即使是仅供内部使用的ALB也需要)以便与Azure ODIC APIs进行交互。
接下来,我们将在Serverless中连接ALB监听器规则,首先从ALB授权配置开始。
custom:
oidc:
tenantId: '${ssm:/web-app/AZURE_TENANT_ID}'
clientId: '${ssm:/web-app/AZURE_CLIENT_ID}'
clientSecret: '${ssm:/web-app/AZURE_CLIENT_SECRET}'
provider:
name: aws
runtime: python3.9
timeout: 60
alb:
authorizers:
azureAdAuth:
type: oidc
authorizationEndpoint: https://login.microsoftonline.com/${self:custom.oidc.tenantId}/oauth2/v2.0/authorize
issuer: https://login.microsoftonline.com/${self:custom.oidc.tenantId}/v2.0
tokenEndpoint: https://login.microsoftonline.com/${self:custom.oidc.tenantId}/oauth2/v2.0/token
userInfoEndpoint: https://graph.microsoft.com/oidc/userinfo
onUnauthenticatedRequest: authenticate
clientId: ${self:custom.oidc.clientId}
clientSecret: ${self:custom.oidc.clientSecret}
请注意有关custom.oidc属性的引用,这些属性是动态从SSM中获取的,并由上面的Terraform传递给您。接下来,将作验证器连接到Lambda函数的ALB事件。
functions:
api:
handler: webapp.alb_handler
events:
- alb:
listenerArn: !Ref LoadBalancerListener
priority: 1
multiValueHeaders: true
authorizer: azureAdAuth
conditions:
path: "*"
部署后,您将在ALB的规则界面看到类似于以下内容:
K8s ALB Annotations
你也可以在Kubernetes中使用Ingress注释来实现类似的配置。
kind: Ingress
apiVersion: extensions/v1beta1
metadata:
name: ***REMOVED***
namespace: ***REMOVED***
annotations:
alb.ingress.kubernetes.io/auth-idp-oidc: >-
{"secretName":"***REMOVED***","issuer":"https://login.microsoftonline.com/***REMOVED***/v2.0","authorizationEndpoint":"https://login.microsoftonline.com/***REMOVED***/oauth2/v2.0/authorize","tokenEndpoint":"https://login.microsoftonline.com/***REMOVED***/oauth2/v2.0/token","userInfoEndpoint":"https://graph.microsoft.com/oidc/userinfo"}
alb.ingress.kubernetes.io/auth-on-unauthenticated-request: authenticate
alb.ingress.kubernetes.io/auth-scope: openid
alb.ingress.kubernetes.io/auth-session-cookie: AWSELBAuthSessionCookie
alb.ingress.kubernetes.io/auth-session-timeout: '604800'
alb.ingress.kubernetes.io/auth-type: oidc
OIDC Headers
验证请求后,ALB会在将请求转发到目标之前添加包含用户声明的额外头部。 链接的文档 中有一个解码x-amzn-oidc-data头部的示例 - 我做了一些修改来缓存公钥、添加日志记录和验证发行者:
AWS_REGION = os.getenv("AWS_REGION", "us-east-1")
OIDC_ISSUER = os.getenv("OIDC_ISSUER", "")
PUB_KEY = None
def decode_jwt(encoded_jwt):
log = logger.new(name="DecodeJwt")
global PUB_KEY
if not PUB_KEY:
log.debug("Initializing ALB public key")
# Step 1: Get the key id from JWT headers (the kid field)
jwt_headers = encoded_jwt.split(".")[0]
decoded_jwt_headers = base64.b64decode(jwt_headers)
decoded_jwt_headers = decoded_jwt_headers.decode("utf-8")
decoded_json = json.loads(decoded_jwt_headers)
kid = decoded_json["kid"]
# Step 2: Get the public key from regional endpoint
url = f"https://public-keys.auth.elb.{AWS_REGION}.amazonaws.com/{kid}"
req = requests.get(url)
PUB_KEY = req.text
else:
log.debug("Using cached public key")
# Step 3: Get the payload
log.debug("Decoding JWT...")
payload = jwt.decode(encoded_jwt, PUB_KEY, issuer=OIDC_ISSUER, algorithms=["ES256"])
log.debug("Decoded JWT", payload=payload)
return payload
这里是一个从Azure AD解码负载示例:
{
"sub": "***REMOVED***",
"name": "Randy Westergren",
"family_name": "Westergren",
"given_name": "Randolph",
"picture": "https://graph.microsoft.com/v1.0/me/photo/$value",
"email": "randy.westergren@***REMOVED***.com",
"exp": 1665076372,
"iss": "https://login.microsoftonline.com/***REMOVED***/v2.0"
}
你可以用这个来记录用户请求,或者扩展该应用程序以添加授权规则。
Login
现在,当您访问受保护的路径时,您将首先被重定向到Azure AD登录:
成功登錄後,後續請求將設置AWSELBAuthSessionCookie
cookie。