Day 09-用 Owner 权限跑 Terraform 等於用 root 权限跑後端,夜路跑多了迟早遇到鬼

用 Owner 权限跑 Terraform 等於用 root 权限跑後端,夜路跑多了迟早遇到鬼

CI/CD 系列

  • day 08-Code 要 Review,Infrastrcture 岂不 Review?吾未见其明也
  • day 09-用 Owner 权限跑 Terraform 等於用 root 权限跑後端,夜路跑多了迟早遇到鬼
  • day 10-(暂定:github action CI/CD automation)
  • day 11-(暂定:atlantis plan apply automation)
  • day 12-(暂定:tfsec automatic security check)

课程内容与代码会放在 Github 上: https://github.com/chechiachang/terraform-30-days

赛後文章会整理放到个人的部落格上 http://chechia.net/

追踪粉专可以收到文章的主动推播

https://ithelp.ithome.com.tw/upload/images/20210901/20120327NvpHVr2QC0.jpg


本章介绍如何建立独立的 service principal (service account / iam role) 专门给 terraform 执行。

前面的例子,我们执行 Terraform 都是用 azure ad User 的身份执行。

Review: 我们使用 az-cli az login,让 terraform 使用本地的 credential 档案。也就是说 terraform 是使用 login user 的权限来运行。我们可以检查一下目前使用者的 Azure 权限

az role assignment list --include-inherited --assignee 12345678-1234-1234-1234-123456789012
[
  {
    "principalName": "chechia_chechia.net#EXT#@chechianet.onmicrosoft.com",
    "principalType": "User",
    "roleDefinitionName": "User Access Administrator",
    "scope": "/",
    "type": "Microsoft.Authorization/roleAssignments"
  }
]

Azure AD 的权限则需要通过 Azure portal 查看

  • Azure portal -> Azure Active Directory -> User -> Assigned Roles
  • 看到 AAD role 是 Global administrator

Global administrator 基本上是超级管理员,权限其实蛮大的,Terraform 的操作并不需要这麽大的权限。

Issues with Global Admin / POLP(Principle of least privilege)

Linux 管理常说,没必要不要使用 root 的权限。

Terraform 也是同样道理。没事不要开着权限很大的帐号乱逛。

administrator / owner 权限太大,什麽事都能做很方便。但实务上常常使用 Admin role 其实是有相当风险的,使用 administrator role 操作 Terraform ,违反最小授权原则(POLP: Principle of least privilege)

所有 credential 只要使用,就可能有泄漏或被害的风险,只是管理方式不同,风险高低差异

  • 考虑 user credential 不小心泄漏 (exposed),对全公司的风险
    • administrator 有关闭整个 project 的权限
    • User Access Administrator 有权更改其他 User 的 RBAC,可能窜改 iam 例如:会把其他 admin 移除,绑架整个公有云

我们上堂课展示过 CI / CD

  • 如果要在 Github Action 上有完整的 terraform 执行权限,自然需要把 azure credential 放到 Github Action 上,才能透过 Github Action 操作 terraform plan 与 apply
  • 比起个人电脑上的 user credential,放到 Github 上增加暴露的风险
    • 这边是讨论风险,当然是相信 Github Action secret 管理安全才会使用
    • 信任解决方案,与事先考量风险管理是不冲突的(超前部署)

通常 iam / RBAC 管理的权限,与其他 resource provision 的权限会切开,特别管理,连工程师的 User 都不应该有 iam 管理权限

许多整合性的角色权限 administrator, owner, editor, collaborater, ... 之类的权限其实都太大,最佳实践是用到什麽权限,就开什麽权限

NOTE:

  • Azure AD + Azure RBAC 管理在其他公有云近似於 GCP IAM / AWS IAm role
  • Azure service principal 在其他公有云近似於 GCP service account / AWS iam role

Prepate Service Principal for Terraform

在实务上,我们为 terraform 设定专属的 service principal。

我们会需要产生 terraform 专属的 service principal

登入认证使用非对称加密的 rsa key/crt,远比起传统密码更加安全

  • 首先使用 openssl 产生 key, csr, crt
    • 上传 public crt 档案到 azure
    • 本地保留 .key,这只 private key 就代表 service principal 的完整权限,妥善保管不要外流
  • 产生 terraform config 需要的 pfx 格式
    • export pfx key 时会需要输入密码,让每只私钥 export 时都有密码加密是好习惯
    • 这个密码要记下来,等等 terraform config 时会用到
  • 范例中产生的路径都放在 ~/.ssh,请找一个更适合的地方收藏
KEY_NAME=~/.ssh/terraform-30-days

openssl req -newkey rsa:4096 -nodes -subj '/CN=terraform-30-days' -keyout ${KEY_NAME}.key -out ${KEY_NAME}.csr
openssl x509 -signkey ${KEY_NAME}.key -in ${KEY_NAME}.csr -req -days 365 -out ${KEY_NAME}.crt
openssl pkcs12 -export -out ${KEY_NAME}.pfx -inkey ${KEY_NAME}.key -in ${KEY_NAME}.crt

Getting Private key
Enter Export Password:
Verifying - Enter Export Password:

ls ~/.ssh/terraform-30-days*
/Users/che-chia/.ssh/terraform-30-days.crt
/Users/che-chia/.ssh/terraform-30-days.csr
/Users/che-chia/.ssh/terraform-30-days.key
/Users/che-chia/.ssh/terraform-30-days.pfx

Provision service principal

产生完後我们使用 terraform 来创建 azure service principal。这里还是使用有 admin 权限的 User 帐号来操作 terraform,范例在 azure/foundation/servcie_principal,看一下内容

  • 为 service principal 命名为 terraform-30-days
  • enable_service_principal_certificate=true 使用 certificate 来认证
    • 如果为 false,则会产生传统密码,使用密码登入
    • 指定本地 certificate 的路径,terraform provision service principal 时会上传
  • role 指派 Contributor,是 azure 内建的 role,没有 Azure RBAC 的权限,比 owner role 好一点,但其实还是权限过大,先当作范例
# azure/foundation/service_principal

  service_principal_name               = "terraform-30-days"
  enable_service_principal_certificate = true
  # https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/guides/service_principal_client_certificate)
  certificate_path                     = "/Users/che-chia/.ssh/terraform-30-days.crt"
  password_rotation_in_years           = 1

  # Adding roles to service principal
  # The principle of least privilege
  role_definition_names = [
    "Contributor"
  ]

这里参考 Github kumarvna/terraform-azuread-service-principal 来修改的是 repo 内的 module。module 本身有放上 Terraform Registry 所以可以直接使用,我自己调整内容所以放到本 repo。细节可以看 azure/modules/azuread_service_principal

确定没问题後 provision resource

terragrunt init
terragrunt plan
terragrunt apply

Azure 上就会产生 service principal,使用 certificate 做登入认证

Test az login

测试一下 az-cli 是否能够登入 service principal,Azure 官方文件:Service Principal

  • 首先把 .key 与 .crt 合并成另一只 keycrt 档案
  • az ad sp list 中寻找 service principal 的细节
    • 需要 AD 的 tenant id
    • 需要 service principal 的 name id
  • 然後执行 az login --service-principal,使用 keycrt 档案做认证
  • 登入後拿到 service principal 的登入讯息,此时本地 az-cli credential 就不是 User 的身份了
cat ~/.ssh/terraform-30-days.key > ~/.ssh/terraform-30-days.keycrt
cat ~/.ssh/terraform-30-days.crt >> ~/.ssh/terraform-30-days.keycrt

APP_NAME=terraform-30-days
az ad sp list --display-name ${APP_NAME}

TENANT_ID=$(az ad sp list --display-name ${APP_NAME} | jq -r '.[0].appOwnerTenantId')
SERVICE_NAME=$(az ad sp list --display-name ${APP_NAME} | jq -r '.[0].servicePrincipalNames[0]')

az login --service-principal \
  --username ${SERVICE_NAME} \
  --tenant ${TENANT_ID} \
  --password ~/.ssh/terraform-30-days.keycrt > /tmp/azure-login-profile

[
  {
    "cloudName": "AzureCloud",
    "homeTenantId": "12345678-1234-1234-1234-123456789012",
    "id": "12345678-1234-1234-1234-123456789012",
    "isDefault": true,
    "managedByTenants": [],
    "name": "Microsoft Azure Sponsorship",
    "state": "Enabled",
    "tenantId": "12345678-1234-1234-1234-123456789012",
    "user": {
      "name": "12345678-1234-1234-1234-123456789012",
      "type": "servicePrincipal"
    }
  }
]

目前身份是 service principal,检查一下自身权限 role assignment,看见正确设定的 role: Contributor

az role assignment list

[
  {
    "principalType": "ServicePrincipal",
    ...
    "roleDefinitionName": "Contributor",
    ...
  }
]

Config Terraform to use service principal

接下来要设定 terraform

有两个方法

  • 直接更改 provider.tf 里面的 azurerm {} 把 client id, ceritificate 等等参数填入
  • 使用环境变数传入参数,terraform 会使用 environment variable,overwrite azurerm 内的设定

这边示范使用环境变数,之後夹到 Github Action 上,或是其他 CI 上会比较方便,不用再更改 provider.tf 的原始码

  • 这边可以自己从 az-cli 取得登入资讯
  • export 各个环境变数
  • env | grep ARM 检查刚刚 export 的参数
export ARM_CLIENT_ID="00000000-0000-0000-0000-000000000000"
export ARM_CLIENT_CERTIFICATE_PATH="/Users/che-chia/.ssh/terraform-30-days.pfx"
export ARM_CLIENT_CERTIFICATE_PASSWORD=<password> # change this
export ARM_SUBSCRIPTION_ID="00000000-0000-0000-0000-000000000000"
export ARM_TENANT_ID="00000000-0000-0000-0000-000000000000"

export ARM_CLIENT_ID=$(cat /tmp/azure-login-profile | jq -r '.[0].user.name')
export ARM_CLIENT_CERTIFICATE_PATH="/Users/che-chia/.ssh/terraform-30-days.pfx"
export ARM_CLIENT_CERTIFICATE_PASSWORD=<password> # change this
export ARM_SUBSCRIPTION_ID=$(az account subscription list | jq -r '.[0].subscriptionId')
export ARM_TENANT_ID=$(cat /tmp/azure-login-profile | jq -r '.[0].tenantId')

env | grep ARM

ARM_CLIENT_CERTIFICATE_PATH=/Users/che-chia/.ssh/terraform-30-days.pfx
ARM_CLIENT_ID=
ARM_SUBSCRIPTION_ID=
ARM_TENANT_ID=
ARM_CLIENT_CERTIFICATE_PASSWORD=

如果参数都正常,

terragrunt init && terragrunt plan

尝试更改自己 service principal 的 role,看能不能 Privilege escalation,把自己从 Contributor 变成 Owner。如果可以的话,terraform 可以自己提升权限变成 Owner,然後近来 azure 乱改

# azure/foundation/service_principal

  role_definition_names = [
    "Contributor",
    "Owner" # try Privilege escalation
  ]

然後再次 apply 看看计谋会不会得逞

terragrunt plan
terragrunt apply

│ Error: Could not set Owners
│
│   with azuread_application.main,
│   on application.tf line 4, in resource "azuread_application" "main":
│    4:   owners           = [data.azuread_client_config.current.object_id]

│ Error: authorization.RoleAssignmentsClient#Create: Failure responding to request: StatusCode=403 -- Original Error: autorest/azure: Service returned an error. Status=403 Code="AuthorizationFailed" Message="The client '12345678-1234-1234-1234-123456789012' with object id '12345678-1234-1234-1234-123456789012' does not have authorization to perform action 'Microsoft.Authorization/roleAssignments/write' over scope '/subscriptions/ba8eb346-d19e-4f65-96d9-8c783e6eea61' or the scope is invalid. If access was recently granted, please refresh your credentials."

Plan 都正常,表示 read / refresh azure 上的资源时,service principal 的 contributor 全县市足够的

apply 则回传 403 permission denied 以及相关错误讯息,azure 表示 terraform 的 service principal 没有权限可以调整 RBAC。表示 service principal 权限是受到限制的,符合我们的预期。

之後 Terraform 的操作,我们就不再使用 az login 来产生本地的 credential,而是使用 service principal

  • 各位可以熟悉一下 terraform 环境变数的设定
  • 除了调整 RBAC 时,还是需要开有 rbac 管理权限的 Owner 帐号,其他时间使用 service principal 就足够
  • 觉得每次 export 很麻烦的话,可以自行修改 azure/terragrunt.hcl,这段,具体修改可以参考 Terraform 官方说明文件
# azure/provider

generate "provider" {
  path = "provider.tf"
  if_exists = "overwrite_terragrunt"
  contents = <<EOF
provider "azurerm" {
  features {}
}
EOF
}

但是拜托

密码跟 certificate .key .pfx 拜托拜托不要 commit 到 git 里面,务必每次手动输入
密码跟 certificate .key .pfx 拜托拜托不要 commit 到 git 里面,务必每次手动输入
密码跟 certificate .key .pfx 拜托拜托不要 commit 到 git 里面,务必每次手动输入

很重要所以讲三次,但还是会看到有人 git add 就把 password commit 进去

Homework

  • LOPL,移除 contributor role,参考 Azure 内建的 role 清单,来做更精细的 RBAC,可能的 role
    • Virtual Machine Contributor
    • Network Contributor
    • Storage Account Contributor
    • Storage Blob Data Owner
  • 承上,apply service principal 之後,回去修改以前的范例,是否仍有足够的权限?还是需要再调整?
  • provision 另外一个 service account,只有 RBAC 权限,但没有更改其他 resource 的权限,例如:不能存取 storage, compute, networking, ...

References


<<:  DAY9 - BFS应用

>>:  [Day 1] Dev's Ops 启程前言

GitHub Actions 基本介绍 - 开始自动化 workflow 的第一步

使用 GitHub Actions 可以让 GitHub Repo 内自订且自动执行你的软件开发流程...

Flutter基础介绍与实作-Day13 Onboarding、Login、Sign Up范例实作(1)

今天我们就利用我们之前所学的来做一个和旅游相关的Onboarding介面,事不宜迟赶快开始吧! 我想...

[Day10] 如何实现图片填色功能 (完结)

#733 - Flood Fill 连结: https://leetcode.com/proble...

Day 14. UX/UI 设计流程之四: Wireflow,并以 Axure RP 实作 (上)

由於 UI Flow 一定程度上已经交代了操作流程会走过哪些页面,接下来设计师就可以根据 UI F...

Day10 - 为什麽官方不推荐使用 getInitialProps

getInitialProps 是较为老旧的 API Next.js 9.3 版本後,官方释出了两个...