external-dns: TXTレコードの暗号化でDNS管理情報をセキュアに保つ

[前回の記事]では、external-dnsを使ってKubernetesリソースからAWS Route53のDNSレコードを自動生成する基本的な方法を紹介しました。その中で、external-dnsが管理に使用するTXTレコードが公開されてしまう点について触れましたが、本記事ではその解決策として、TXTレコードの暗号化機能の実装方法を詳しく解説します。


[前回の記事]: https://qiita.com/suin/items/4fad0e3335cd34a26382

## TXTレコードとRegistryについて

external-dnsは、DNSレコードの管理情報をRegistryと呼ばれる場所に保存します。このRegistryには、TXTレコード以外にもDynamoDBやAWS Service Discoveryなどを使用することができます。

TXT Registryを選択する利点は、これ自身がDNSのプロトコルにしか依存していないおかげで、クラウドベンダー固有のサービスを避けられることです。これによりベンダーロックインを回避しつつ、管理情報を保持することができます。ただし、TXTレコードはパブリックに公開されるため、そのままでは管理情報が外部から見えてしまいます。そこで、TXTレコードの暗号化機能を使用することで、この問題を解決できます。

## 前提条件

- Kubernetesクラスターがセットアップされていること
- 必要なコマンドツールがインストールされていること
    - kubectl
    - helm
    - aws
    - dig
- AWSアカウントのアクセスキーとシークレットキー

## セットアップ手順

### 1. AWS Route53 Hosted Zoneの作成

まず、AWSの認証情報を環境変数に設定します：

```bash
export AWS_ACCESS_KEY_ID=AKIAXXXXXXXXXXXXXXXX
export AWS_SECRET_ACCESS_KEY=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
```

認証情報が正しく設定されているか確認します：

```bash
aws sts get-caller-identity
```

AWS Route53でHosted Zoneを作成します：

```bash
aws route53 create-hosted-zone --name example-tutorial.com --caller-reference external-dns-tutorial-$(date +%s)
```

**Note:** caller-referenceは各作成試行で一意である必要があります。ここではタイムスタンプを使用して一意性を確保しています。

### 2. external-dnsのインストール（TXTレコード暗号化有効）

Bitnamiが提供するHelmチャートを使用します。このチャートは、kubernetes-sigsが提供するものよりもオプションが豊富で、AWS Route53の設定が容易です：

```bash
helm install external-dns \
  --set provider=aws \
  --set aws.zoneType=public \
  --set aws.credentials.accessKey="$AWS_ACCESS_KEY_ID" \
  --set aws.credentials.secretKey="$AWS_SECRET_ACCESS_KEY" \
  --set txtOwnerId=example-owner-id-123 \
  --set "domainFilters[0]=example-tutorial.com" \
  --set policy=sync \
  --set "sources[0]=crd" \
  --set crd.create=true \
  --set crd.apiversion=externaldns.k8s.io/v1alpha1 \
  --set crd.kind=DNSEndpoint \
  --set txtEncrypt.enabled=true \
  --set txtEncrypt.aesKey="" \
  oci://registry-1.docker.io/bitnamicharts/external-dns
```

#### インストールオプションの説明

| オプション | 説明 |
|------------|------|
| `provider=aws` | AWS Route53を使用することを指定 |
| `aws.zoneType=public` | パブリックゾーンを使用することを指定 |
| `aws.credentials.accessKey` | AWSのアクセスキー |
| `aws.credentials.secretKey` | AWSのシークレットキー |
| `txtOwnerId` | TXTレコードのオーナーID（任意の値を指定可能） |
| `domainFilters[0]` | 監視対象のドメイン |
| `policy=sync` | DNSレコード同期ポリシー |
| `sources[0]=crd` | CRDを使用することを指定 |
| `crd.create=true` | CRDの作成を有効化 |
| `crd.apiversion` | CRDのAPIバージョン |
| `crd.kind` | CRDの種類 |
| `txtEncrypt.enabled=true` | TXTレコードの暗号化を有効化 |
| `txtEncrypt.aesKey` | 暗号化用のAESキー（空文字列の場合は自動生成） |

インストール後、external-dnsのログを確認して正常に起動していることを確認します：

```bash
kubectl logs -l app.kubernetes.io/name=external-dns -f
```

次のようなログが表示されれば、暗号化機能が有効な状態で正常に起動しています：

```
time="2024-11-06T07:03:47Z" level=info msg="config: {...TXTEncryptEnabled:true...}"
time="2024-11-06T07:03:47Z" level=info msg="Instantiating new Kubernetes client"
time="2024-11-06T07:03:47Z" level=info msg="Using inCluster-config based on serviceaccount-token"
time="2024-11-06T07:03:47Z" level=info msg="Created Kubernetes client https://10.43.0.1:443"
time="2024-11-06T07:03:49Z" level=info msg="Applying provider record filter for domains: [example-tutorial.com. .example-tutorial.com.]"
```

AES暗号化キーが生成され、Secretに保存されていることを確認します：

```bash
kubectl get secret external-dns -o yaml
```

Secretの中に `txt_aes_encryption_key` フィールドが存在していることを確認できます：

```yaml
apiVersion: v1
kind: Secret
data:
  txt_aes_encryption_key: Q2NCSUF6c2I1N215SGY4RWZtWmZvWm1keUl2SHBsTDY=  # Base64エンコードされたAESキー
# ... 他のフィールドは省略
```

### 3. DNSレコードの作成

external-dnsがDNSレコードを作成することを確認します。今回は実体リソースが不要なCRDのDNSEndpointを使用します：

```yaml
# test.example-tutorial.com.yaml
apiVersion: externaldns.k8s.io/v1alpha1
kind: DNSEndpoint
metadata:
  name: test.example-tutorial.com
spec:
  endpoints:
  - dnsName: test.example-tutorial.com
    recordTTL: 180
    recordType: A
    targets:
    - 127.0.0.1
```

このマニフェストを適用します：

```bash
kubectl apply -f test.example-tutorial.com.yaml
```

しばらくすると、external-dnsがDNSレコードを作成します。ログを確認します：

```
time="2024-11-06T07:12:52Z" level=info msg="Desired change: CREATE a-test.example-tutorial.com TXT" profile=default zoneID=/hostedzone/Z00418233KGBJI8AZJFPR zoneName=example-tutorial.com.
time="2024-11-06T07:12:52Z" level=info msg="Desired change: CREATE test.example-tutorial.com A" profile=default zoneID=/hostedzone/Z00418233KGBJI8AZJFPR zoneName=example-tutorial.com.
time="2024-11-06T07:12:52Z" level=info msg="2 record(s) were successfully updated" profile=default zoneID=/hostedzone/Z00418233KGBJI8AZJFPR zoneName=example-tutorial.com.
```

AWS CLIを使用してDNSレコードが作成されたことを確認します：

```bash
aws route53 list-resource-record-sets --hosted-zone-id Z00418233KGBJI8AZJFPR
```

AレコードとTXTレコード（暗号化済み）の両方が確認できます：

```json
{
    "ResourceRecordSets": [
        {
            "Name": "test.example-tutorial.com.",
            "Type": "A",
            "TTL": 180,
            "ResourceRecords": [
                {
                    "Value": "127.0.0.1"
                }
            ]
        },
        {
            "Name": "a-test.example-tutorial.com.",
            "Type": "TXT",
            "TTL": 300,
            "ResourceRecords": [
                {
                    "Value": "\"YwPTDxmRgtKjryuSqYrqA35DoRkFw94ZxoojvZ9goHiyXbd8zYS8wBqS7t3ZtZoqREqDDaLtLcB0wbzTpw9n1+HxgGrJc795b4ISnJXRI03+sJ+DgN71dU7hCCyoPx25w/jYbOX3/zP DP59BmZaAly/OLmCEcDTW7dl697qdj4lsNHBrr+6Z1lAFKHAKfX3pM9w6RFGmpGl4WULtAA==\""
                }
            ]
        }
    ]
}
```

TXT暗号化を有効にしたことで、TXTレコードの内容はAES暗号化されています。このTXTレコードには従来と同じ管理情報（heritage、owner、resourceなど）が含まれていますが、暗号化されているため外部からの参照はできません。

### クリーンアップ

#### 1. DNSレコードの削除

CRDリソースを削除します：

```bash
kubectl delete -f test.example-tutorial.com.yaml
```

external-dnsのログで削除が完了したことを確認できます：

```
time="2024-11-06T05:48:24Z" level=info msg="Desired change:  DELETE  a-test.example-tutorial.com TXT" profile=default zoneID=/hostedzone/Z08033563HFN15GSXJ766 zoneName=example-tutorial.com.
time="2024-11-06T05:48:24Z" level=info msg="Desired change:  DELETE  test.example-tutorial.com A" profile=default zoneID=/hostedzone/Z08033563HFN15GSXJ766 zoneName=example-tutorial.com.
time="2024-11-06T05:48:24Z" level=info msg="Desired change:  DELETE  test.example-tutorial.com TXT" profile=default zoneID=/hostedzone/Z08033563HFN15GSXJ766 zoneName=example-tutorial.com.
time="2024-11-06T05:48:24Z" level=info msg="3 record(s) were successfully updated" profile=default zoneID=/hostedzone/Z08033563HFN15GSXJ766 zoneName=example-tutorial.com.
```

#### 2. external-dnsの削除

Helmリリースを削除します：

```bash
helm uninstall external-dns
```

#### 3. Hosted Zoneの削除

Hosted Zoneの一覧を確認し：

```bash
aws route53 list-hosted-zones
```

確認したIDを使用してHosted Zoneを削除します：

```bash
aws route53 delete-hosted-zone --id Z00418233KGBJI8AZJFPR
```

## おわりに

本記事では、external-dnsのTXTレコード暗号化機能を使用して、DNS管理情報をセキュアに保つ方法を解説しました。TXTレコードは、クラウドベンダー固有のサービスを避けつつ管理情報を保持できる便利なRegistryですが、そのままでは情報が公開されてしまいます。暗号化機能を使用することで、この問題を解決しつつTXT Registryのメリットを活かすことができます。

[前回の記事]と合わせて参照いただくことで、より理解が深まるかと思います。ぜひ実際に試してみてください！