【DevOps云实践】IaaC:在AWS Application Load Balancer上实现Azure AD的OIDC SSO认证

2024-09-10T13:11:32+08:00 | 4分钟阅读 | 更新于 2024-09-10T13:11:32+08:00

Macro Zhao

【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。

© 2011 - 2025 Macro Zhao的分享站

关于我

如遇到加载502错误,请尝试刷新😄

Hi,欢迎访问 Macro Zhao 的博客。Macro Zhao(或 Macro)是我在互联网上经常使用的名字。

我是一个热衷于技术探索和分享的IT工程师,在这里我会记录分享一些关于技术、工作和生活上的事情。

我的CSDN博客:
https://macro-zhao.blog.csdn.net/

欢迎你通过评论或者邮件与我交流。
Mail Me

推荐好玩(You'll Like)
  • AI 动·画

    • 这是一款有趣·免费的能让您画的画中的角色动起来的AI工具。
    • 支持几十种动作生成。
  • AI 识字

    • 遇到不认识的字,写在这里,会自动识别所写汉字。
    • 还能对其进行意义查询。
  • 中日假期日历

    • 方便查询2025日本红日子。
    • 对日打工族必备工具。
  • 在线架子鼓

    • 简易但功能齐全的架子鼓。
    • 代码敲累了,你就敲一敲它吧。
  • 微信公众号编辑器

    • 简易但功能丰富的免费!公众号编辑器。
    • 还在不断完善中,喜欢的可以持续关注.
我的项目(My Projects)
  • 爱学习网

  • 小乙日语App

    • 这是一个帮助日语学习者学习日语的App。
      (当然初衷也是为了自用😄)
    • 界面干净,简洁,漂亮!
    • 其中包含 N1 + N2 的全部单词和语法。
    • 不需注册,更不需要订阅!完全免费!
  • 小乙日文阅读器

    • 词汇不够?照样能读日语名著!
    • 越读积累越多,积跬步致千里!
    • 哪里不会点哪里!妈妈再也不担心我读不了原版读物了!
赞助我(Sponsor Me)

如果你喜欢我的作品或者发现它们对你有所帮助,可以考虑给我买一杯咖啡 ☕️。这将激励我在未来创作和分享更多的项目和技术。🦾

👉 请我喝一杯咖啡

If you like my works or find them helpful, please consider buying me a cup of coffee ☕️. It inspires me to create and share more projects in the future. 🦾

👉 Buy me a coffee