Azure AD の条件付きアクセスを Azure CLI で作成して Azure DevOps の PAT がどうなるか試してみた

## 背景と目的

Azure AD の条件付きアクセス良いですよね。パブリッククラウドを使い出した 2011 年からしばらくは、パブリッククラウドへのサインイン等のアクセスを IP アドレスで制限したいニーズは、それまでのオンプレで IP アドレス制限していた背景からとても多くありました。そして時代は Subversion から Git に移り Git リポジトリ自体は未だにオンプレもしくはプライベート IP アドレスでしか使えない仮想マシン上に構築した Git リポジトリを使っているケースが多いと感じています。そのうち Git リポジトリ自体もパブリッククラウドにあるものを使うのが当たり前になると思っていますが、そこで問題になるのが PAT (パーソナルアクセストークン) です。PAT はパスワードみたいなものだから、知っていたらどこからでもアクセスできてしまうのではないか？と。いやいや、そんな事はありません、ネームドロケーションで信頼された場所以外からのアクセスは条件付きアクセスで防いでくれるはず。ということで、Azure AD の条件付きアクセスを Azure CLI で作成して Azure DevOps の PAT がどうなるか試してみました。

## 検証用の仮想マシンを作成

```bash:bash
# 環境変数をセットします
region=japaneast
prefix=mnrcatest

# リソースグループを作成します
az group create \
  --name ${prefix}-rg \
  --location $region

# 仮想マシンを 2 つ作成します
for i in {1..2}; do
  az vm create \
    --resource-group ${prefix}-rg \
    --name ${prefix}-vm${i} \
    --os-disk-name ${prefix}-vm${i}OSDisk \
    --image Canonical:UbuntuServer:18.04-LTS:latest \
    --size Standard_B1s \
    --admin-username azureuser \
    --generate-ssh-keys \
    --nsg-rule NONE \
    --public-ip-address-dns-name ${prefix}${i}
done

# 自分の IP から SSH 接続できるようにします
for i in {1..2}; do
  az network nsg rule create \
    --resource-group ${prefix}-rg \
    --name Allow-SSH \
    --nsg-name ${prefix}-vm${i}NSG \
    --priority 100 \
    --source-address-prefixes $(curl -s inet-ip.info) \
    --destination-port-ranges 22 \
    --access Allow \
    --protocol Tcp
done
```

## 検証用のテストユーザーを作成

```bash:bash
# テストユーザーのエイリアスを設定します
useralias="catest"

# UPN に使用するドメインを選択します
domain=$(az rest \
  --method get \
  --uri https://graph.microsoft.com/v1.0/domains \
  --query value[1].id \
  --output tsv)

# AD ユーザーが使用するパスワードを生成します
userpasswd=$(openssl rand -base64 16)
echo $userpasswd

# AD ユーザーを作成します
az ad user create \
  --display-name $useralias \
  --password $userpasswd \
  --user-principal-name $useralias@$domain
```

## 条件付きアクセスを作成

```bash:bash
# VM1 の PublicIP を取得する
az network public-ip show \
  --resource-group ${prefix}-rg \
  --name ${prefix}-vm1PublicIP \
  --query ipAddress \
  --output tsv

# VM1 の PublicIP を信頼できる場所として IP ネームドロケーションを作成する
# https://learn.microsoft.com/ja-jp/graph/api/conditionalaccessroot-post-namedlocations?view=graph-rest-1.0&tabs=http#example-1-create-an-ipnamedlocation
az rest \
  --method post \
  --uri https://graph.microsoft.com/v1.0/identity/conditionalAccess/namedLocations \
  --body '{
    "@odata.type": "#microsoft.graph.ipNamedLocation",
    "displayName": "Trusted IP named location",
    "isTrusted": true,
    "ipRanges": [
      {
        "@odata.type": "#microsoft.graph.iPv4CidrRange",
        "cidrAddress": "20.210.118.229/32"
      }
    ]
  }'

# テストユーザーの ID を取得する
az ad user list \
  --display-name $useralias \
  --query [].id \
  --output tsv

# テストユーザーのみ信頼できる場所以外を全てブロックするポリシーを作成
# https://learn.microsoft.com/ja-jp/graph/api/conditionalaccessroot-post-policies?view=graph-rest-1.0&tabs=http#examples
az rest \
  --method post \
  --uri https://graph.microsoft.com/v1.0/identity/conditionalAccess/policies \
  --body '{
    "displayName": "catest user policy",
    "state": "enabled",
    "conditions": {
      "clientAppTypes": [
        "all"
      ],
      "applications": {
        "includeApplications": [
          "All"
        ]
      },
      "users": {
        "includeUsers": [
          "a36cfbc5-a20e-4949-8599-f33bffa10ec9"
        ]
      },
      "locations": {
        "excludeLocations": [
          "AllTrusted"
        ],
        "includeLocations": [
          "All"
        ]
      }
    },
    "grantControls": {
      "operator": "OR",
      "builtInControls": [
        "block"
      ]
    }
  }'
```

## Azure AD の条件付きアクセスと Azure DevOps を連携

Azure DevOps の Organization settings から下記の設定（Enable Azure Active Directory Conditional Access Policy Validation）をオンにします。

![devops-conditional-access-01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/675885/2745c2da-0dcc-a40d-4cdc-15d2c80bc182.png)

## 検証用ユーザーで PAT を作成

何かしらの方法で検証用ユーザーを既存の Azure DevOps にアクセスできるようにし、検証用ユーザーで Azure DevOps にサインインして PAT を作成します。

信頼できる場所にブラウザからアクセスするロケーションが含まれていない場合、検証用ユーザーで Azure DevOps にサインインすると下記のようになるので、自分自身の IP アドレスも先ほど作成した「Trusted IP named location」に追加してみてください。

![devops-conditional-access-02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/675885/b45c3c0b-1cb9-ecc5-152a-3b2d0a08616e.png)

## Azure DevOps の PAT で git clone を試す

```bash:bash
# VM1 に SSH 接続します
ssh azureuser@${prefix}1.$region.cloudapp.azure.com

# 信頼できる場所に登録しているので問題なく git clone できます
git clone < git url >

# VM1 から抜けます
exit

# VM2 に SSH 接続します
ssh azureuser@${prefix}2.$region.cloudapp.azure.com

# 信頼できる場所に登録していないので git clone が失敗します
git clone < git url >

# 下記がエラーメッセージ
fatal: unable to access '< git url >': The requested URL returned error: 403

# VM2 から抜けます
exit
```

## 検証したリソースを削除

```bash:bash
# 作成したポリシーを削除します
az rest \
  --method delete \
  --uri https://graph.microsoft.com/v1.0/identity/conditionalAccess/policies/$(az rest \
  --method get \
  --uri https://graph.microsoft.com/v1.0/identity/conditionalAccess/policies \
  --query "value[?displayName == 'catest user policy'].id" \
  --output tsv)

# 作成したネームドロケーションを削除します
az rest \
  --method delete \
  --uri https://graph.microsoft.com/v1.0/identity/conditionalAccess/namedLocations/$(az rest \
  --method get \
  --uri https://graph.microsoft.com/v1.0/identity/conditionalAccess/namedLocations \
  --query "value[?displayName == 'Trusted IP named location'].id" \
  --output tsv)

# 検証用ユーザーを削除します
az ad user delete \
  --id $useralias@$domain

# 検証環境を削除します
az group delete \
  --name ${prefix}-rg \
  --yes
```

## 参考

https://learn.microsoft.com/ja-jp/azure/devops/organizations/accounts/change-application-access-policies?view=azure-devops