[Ansible]Cloudflare Zero Trust配下のAAP設定をGitLabで管理②～Cloudflare・GitLab設定～

本記事は以下2部構成で作成しています。

[[Ansible]Cloudflare Zero Trust配下のAAP設定をGitLabで管理①～Playbook作成～](https://qiita.com/miyuk/items/24e2afee90ffe61c2b93)
[[Ansible]Cloudflare Zero Trust配下のAAP設定をGitLabで管理②～Cloudflare・GitLab設定～](https://qiita.com/miyuk/items/d0d21bd0175b875aebe1)(本記事)


# はじめに

全体の構成は以下の通りです。
詳細は[前記事](https://qiita.com/miyuk/items/24e2afee90ffe61c2b93)を参照ください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/335255/992c6ba1-6a99-e5ca-648d-a152b4a09e4e.png)

[前記事](https://qiita.com/miyuk/items/24e2afee90ffe61c2b93)では、AAPでWebhookを受け取れるようにしました。
本記事では、Cloudflare Zero Trust(以降Cloudflareという)経由でGitLab RunnerからWebhookを送れるようにします。(図の②、③)



# 構築

## ②CloudflareでGitLab通信のみ通信許可

まずは、外部からの通信をCloudflareで制御してGitLab Runnerからの通信のみAAPへ通すようにします。

### Service Token作成

Cloudflareでは、原則としてWebアクセスはSAMLなどのユーザ認証に成功したユーザアクセスのみがポリシー評価対象となり、それ以外はポリシー評価すらされません。
そのため、今までのような`特定IPアドレスレンジは問答無用で許可`というような制御は出来ません。

しかし、それではユーザ認証ができないサービスアプリケーションから利用できないため、`Service Token`という仕組みをが用意されています。

これは、Cloudflareがサービス用に発行したID(`CF-Access-Client-Id`)とシークレット(`CF-Access-Client-Secret`)をヘッダーに加えることでどのアプリケーションからのアクセスかを識別できるようにするものです。
具体的には上記ヘッダーによってJWT生成してCookieに保存してそちらで識別します。

Cloudflare Zero Trustダッシュボードより、`Access` > `Service Auth` > `Service Tokens`にアクセスしてトークンを作成します。

作成すると以下のようにIDとシークレットが表示されます。
シークレットはこのタイミングでしか表示されないのでコピー必須です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/335255/73a4e272-2ece-b3c7-0cd6-4b9f6df091e8.png)

# アプリケーションポリシー設定

先程作成した`Service Token`を使った通信を許可するようにアプリケーションポリシーを設定します。

事前に以下の公式ドキュメントを参考にAAP用のアプリケーションを作成しておきます。

https://developers.cloudflare.com/cloudflare-one/applications/configure-apps/self-hosted-apps/


Cloudflare Zero Trustダッシュボードより`Access` > `Applications` > `<AAP用のアプリケーション>` > `Policies`にアクセスして新規ポリシーを作成します。

設定項目は以下内容のとおりです。

- `Action`を`Service Auth`にする
- `Create additional rules`に先程作成したService Tokenを指定する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/335255/6eae133e-b437-89b5-0265-e49115239f43.png)

これで、Service Tokenを使った通信は許可されWebhookを送信することが出来ます。

実際に手元のPCから以下curlを送ることでWebhookを実行できました。

```bash:curlコマンド
curl <Webhook URL> -X POST \
 -v \
 -H "Content-Type: application/json" \
 -H "X-Gitlab-Token: <Webhookキー>" \
 -H "CF-Access-Client-Id: <CFトークンID>" \
 -H "CF-Access-Client-Secret: <CFトークンシークレット>" \
 -d "{\"test\": \"test_cf\"}"
```

通信内容を見てみると以下の通りでサービストークンの通信をユーザ認証せずにそのまま許可してAAPのコンテンツを返しています。

また、レスポンスの中でCookieに`CF_Authorization`が追加されJWTが入っています。
これで以降Cookieの有効期限まではサービストークンを使わなくてもCookieを使えば通信が許可されます。

```bash:curlコマンド結果
*   Trying 172.67.194.15:443...
* Connected to <AAP FQDN> (172.67.194.15) port 443 (#0)
# 省略
> POST <Webhook URLパス> HTTP/2
> Host: <AAP FQDN>
> user-agent: curl/7.76.1
> accept: */*
> content-type: application/json
> x-gitlab-token: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
> cf-access-client-id: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.access
> cf-access-client-secret: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
> content-length: 21
>
# 省略
< set-cookie: CF_Authorization=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx; Expires=Fri, 22 Sep 2023 23:23:36 GMT; Path=/; Secure; HttpOnly; SameSite=none
< access-control-expose-headers: X-API-Request-Id
< allow: POST, OPTIONS
< content-language: en
< vary: Accept, Accept-Language, Origin, Cookie
< x-api-node: xxxxxxxx
< x-api-product-name: Red Hat Ansible Automation Platform
< x-api-product-version: 4.2.0
< x-api-request-id: xxxxxxxxxxxxxxxxxxxxx
< x-api-time: 0.082s
< x-api-total-time: 0.193s
< cf-cache-status: DYNAMIC
< report-to: {"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=xxxxxxxx"}],"group":"cf-nel","max_age":604800}
< nel: {"success_fraction":0,"report_to":"cf-nel","max_age":604800}
< server: cloudflare
< cf-ray: xxxxx-NRT
< alt-svc: h3=":443"; ma=86400
<
# 省略
```

## ③GitLabからmain pushでWebhookをAAPに送信

最後に、GitLabからのWebhookを送信できるように設定します。

### GitLabの変数定義

GitLabから今まで作成したWebhook URLやトークン情報をCI/CDで使えるように変数定義します。

全記事の①で作成したAAP構築プロジェクトページより、`設定` > `CI/CDの設定` > `変数`にアクセスして以下のような変数を作成します。
トークン情報は、`マスク`属性をつけることで誤ってログなどに表示することを防ぐようにしておきます。

|キー|値|属性|
|---|---|---|
|AAP_WEBHOOK_URL|AAPのWebhook URL|`保護` `展開`|
|GITLAB_TOKEN|AAPのWebhookトークン|`保護` `マスク` `展開`|
|CF_ACCESS_CLIENT_ID|CloudflareのトークンID|`保護` `マスク` `展開`|
|CF_ACCESS_CLIENT_SECRET|Cloudflareのトークンシークレット|`保護` `マスク` `展開`|

これで、GitLabから変数が使えるようになりました。

### GitLab Runner設定

AAPのGitLab Webhookサービスは、もともとはGitLabの`Webhook機能`を使うことを想定しています。

しかし、②で定義したCloudflareトークンはヘッダー形式で設定する必要があり、GitLabではそれを設定することが出来ません。
(一応、以前から[議論はされている](https://gitlab.com/gitlab-org/gitlab/-/issues/17290)ようですが全く実装されていないようです。)

そのため、今回は`.gitlab-ci.yml`を作成して、パイプラインからcurlコマンドで実行するようにしました。

先程作成した環境変数を使って以下のような`.gitlab-ci.yml`を作成します。

```yaml:.gitlab-ci.yml
workflow:
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

stages:
  - deploy

deploy-aap:
  stage: deploy
  script:
    - >-
      curl ${AAP_WEBHOOK_URL}
      -X POST
      -L
      -v
      -H "Content-Type: application/json"
      -H "CF-Access-Client-Id: ${CF_ACCESS_CLIENT_ID}"
      -H "CF-Access-Client-Secret: ${CF_ACCESS_CLIENT_SECRET}"
      -H "X-Gitlab-Token: ${GITLAB_TOKEN}"
      -d "{\"before\": \"${CI_COMMIT_BEFORE_SHA}\", \"after\": \"${CI_COMMIT_SHA}\"}"
```

これにより、mainにコミットプッシュをトリガーとしてパイプラインが起動してAAP側にジョブが実行されます。

AAP側にどんなheaderが来ているか見たかったので以下のとおり、Nginxのログ出力を変えてヘッダー内容を確認しました。

```bash: nginx.conf
    log_format main '"$request" $status $bytes_sent '
                'content-type: "$http_content_type" \n'
                'cookie: "$http_cookie" \n'
                'x-gitlab-token: "$http_x_gitlab_token" \n'
                'cf-access-client-id: "$http_cf_access_client_id" \n'
                'cf-access-client-secret: "$http_cf_access_client_secret"';
```

```bash:ログ出力
"POST /api/v2/job_templates/69/gitlab/ HTTP/1.1" 202 541 content-type: "application/json"
cookie: "CF_Authorization=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx;"
x-gitlab-token: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
cf-access-client-id: "-"
cf-access-client-secret: "-"
```

Cloudflareのトークン情報はAAP側まで届いておらず、Cloudflare側で取り除かれているようです。
一方、JWT情報(`CF_Authorization`)はサーバ側がCloudflare Zero Trustからの通信であることを証明するためそのまま残っています。

ちなみに、この`CF_Authorization`を使って同じ組織の別アプリケーションにもアクセスしてMITM攻撃出来るか試してみましたが駄目でした。
どうやらアプリケーション or セッションごとにJWT発行しているようです

# さいごに

本記事では、Cloudflare Zero Trust環境下のサーバにGitLabから外部アプリからアクセスする方法を紹介しました。

Cloudflare Zero Trustは機能が多すぎてなかなか検証しづらいですがいざ実際に使ってみるとドキュメントも揃っていて、すんなりと使いこなすことが出来ました。

また、細かいところでもセキュリティを担保するための技術が使われておりとても勉強になりました。

今回はGitLabからの利用でしたが同じようなクラウドとオンプレの通信をゼロトラスト技術で制御することが増えてくると思います。

製品によってこのあたりは思想が違ってくる部分でもあると思うので今後ゼロトラストを導入する上で検証が必要な観点だと感じました。

# 参考リンク

- https://developers.cloudflare.com/cloudflare-one/identity/service-tokens/
    - Cloudflareの`Service Tokens`についての説明
- https://developers.cloudflare.com/cloudflare-one/identity/authorization-cookie/validating-json/
    - CloudflareのJWTをサーバから検証する説明
- https://gitlab-docs.creationline.com/ee/ci/yaml/
    - GitLabのCI/CDパイプラインの説明
- https://gitlab-docs.creationline.com/ee/ci/variables/
    - GitLabのCI/CD変数の説明