AlmaLinux 9 一覧へ戻る トップAlmaLinux 9 / Phase 3 図解
AlmaLinux 9 ・ Phase 3 設計解説

Phase 3:Headscale 構築(PostgreSQL版)

Macvlan・Pod内通信・起動順序制御の「なぜ」を図解する

対象: 192.168.2.13(SERVERセグメント) 所要時間目安: 約40〜50分 費用: 完全無料

本ページの位置づけ

本ページはPhase 3(Headscale構築)の設計思想を3つの構造図で解説する資料です。実際の構築で使う最終版の手順・設定ファイルは「ゼロトラスト基盤構築ガイド(v5決定版)」に統合されており、本ページの手順もその内容と整合させています(NIC名の確認、Quadletの起動方法など、トラブルシューティング記録で判明した修正を反映済みです)。

目次

  1. 本システムを紐解く3つの構造図
  2. 構築手順
  3. 正常性のテスト&検証コマンド
  4. 初学者のためのミニ用語解説

1. 本システムを紐解く3つの構造図

図① 物理ホストとコンテナの接続(MacvlanとPod共有ネットワーク)

本構成では、物理NICを論理的に分割する「Macvlan」技術を使用し、コンテナ群にSERVERセグメントと同じセグメントの独立したIP(192.168.2.13)を直接割り当てます。

SERVERセグメント(192.168.2.0/24)/ 物理NIC経由
ホストOS(AlmaLinux:192.168.2.3)
├─ 仮想カード macv0: 192.168.2.99(ホスト⇔コンテナ直接通信バイパス)
仮想グループ headscale-pod(192.168.2.13)

同一のIPアドレスをグループ内の全コンテナで共有

▶ headscale(VPNコントロールプレーン)
▶ headscale-db(PostgreSQLデータベース)

図② 同一Pod内におけるコンテナ間ループバック通信

Headscale本体からデータベース(PostgreSQL)にアクセスする際、接続先として 127.0.0.1(localhost)を指定します。通常コンテナはIPアドレスが異なりますが、同一の「Pod」に同居しているコンテナ同士は、ネットワーク名前空間(Network Namespace)を完全に共有するため、あたかも同一OSの中に同居しているかのように127.0.0.1を通じた超高速な内部通信が可能です。

headscale-pod(IP: 192.168.2.13)
headscale 本体 ──(Noise認証プロトコル)
↓ 内部通信: 127.0.0.1:5432
headscale-db(PostgreSQL)

図③ systemd(systemctl)によるコンテナの起動順序制御

データベースの起動が完了する前にHeadscaleが立ち上がると、データベース接続エラーによってHeadscaleコンテナが起動不全を起こし、再起動を繰り返す「接続ループ地獄」に陥ります。これを防ぐため、次世代サービス定義規格である「Quadlet」を使用し、「データベースが100%起動した後にのみHeadscaleを起動させる」という依存関係をシステムに強制します。

1. システム起動(systemctl start headscale)
2. 依存関係のチェック(Requires / After)
3. 先にデータベースが起動(headscale-db.service)
↓ 完全なプロセス起動を検証
4. Headscale本体が安全に起動(headscale.service)

2. 構築手順

3-1

永続化ディレクトリの準備とセキュリティ初期化

コンテナが消滅したり再作成されたりしても、登録した端末データや暗号鍵が失われないよう、ホストOS側に強固なデータ保存領域(永続化フォルダ)を作成します。

# 1. 保存用フォルダを一括作成
sudo mkdir -p /srv/containers/headscale/config
sudo mkdir -p /srv/containers/headscale/data
sudo mkdir -p /srv/containers/headscale/postgres

# 2. 空のACLファイルを作成(起動時のマウントエラーを防ぐため)
sudo touch /srv/containers/headscale/config/acl.hujson

# 3. 【最重要】SELinuxのセキュリティコンテキスト(アクセスラベル)を初期化
# (これを怠ると、ホストに存在するフォルダにコンテナがアクセスできず、起動に失敗します)
sudo restorecon -Rv /srv/containers/headscale

# 4. 【重要】コンテナ内の実行ユーザー(UID 1000)に所有権を変更
# (コンテナ内で一般ユーザー権限で動作するHeadscaleプロセスに、ファイルの読み書き権限を付与します)
sudo chown -R 1000:1000 /srv/containers/headscale/data
sudo chown 1000:1000   /srv/containers/headscale/config/acl.hujson

# 5. シークレットキーとデータベース用パスワードの生成と固定(パーミッション600)
sudo touch /srv/containers/headscale/.env
sudo chmod 600 /srv/containers/headscale/.env
echo "HEADSCALE_DB_PASS=$(openssl rand -hex 32)" | sudo tee /srv/containers/headscale/.env
3-2

Headscale設定ファイル(config.yaml)の作成と自動パスワード埋め込み

Headscale本体の動作・ルーティング・データベース接続を規定する設定ファイルを作成します。

yourdomain.mydns.jp を書き換えてください

ご自身の無料DDNSドメイン(または所有されているドメイン)に置き換えてください。

sudo tee /srv/containers/headscale/config/config.yaml << 'EOF'
# ── 外部公開URL(CaddyがHTTPS 443で公開するURLと一致させる)──
server_url: https://hs.yourdomain.mydns.jp
# ── コンテナ内部の受付ポート(HTTPのみ・TLSなし)───────
listen_addr: 0.0.0.0:8080
# ── 管理API(外部非公開・Pod内localhostのみ)──────────
grpc_listen_addr: 127.0.0.1:50443
grpc_allow_insecure: true
# ── Prometheusメトリクス(0.0.0.0にして将来Prometheusから収集可能に)
metrics_listen_addr: 0.0.0.0:9090
# ── データベース設定(PostgreSQL)───────────────────
db_type: postgres
db_host: 127.0.0.1
db_port: 5432
db_name: headscale
db_user: headscale
db_pass: 'DB_PASS_PLACEHOLDER'
# ── ACLファイルパス ────────────────────────────────
acl_policy_path: /etc/headscale/acl.hujson
# ── DERP(中継サーバー)─────────────────────────────
derp:
  server:
    enabled: false
# ── タイムアウト設定 ────────────────────────────────
node_key_expiry: 180d
ephemeral_node_inactivity_timeout: 30m
# ── 【必須】WireGuard通信用秘密鍵
# トラブルシューティング問題④:この設定がないと"private key path=空欄"エラーで起動失敗する
# 鍵ファイルはHeadscaleが初回起動時に自動生成する
private_key_path: /var/lib/headscale/private.key
# ── 【必須】Tailscale v2プロトコル用ノイズ暗号鍵
# トラブルシューティング問題③:Headscale 0.22.xから必須になった設定
# 鍵ファイルはHeadscaleが初回起動時に自動生成する
noise:
  private_key_path: /var/lib/headscale/noise_private.key
EOF

設定ファイルを自動的に書き換え、パスワードを完全に隠蔽するコマンド

ホスト側で.env内に保存したランダムパスワードを、手作業での書き間違いを防ぐために、一括置換スクリプト(sed)で安全にconfig.yamlへ埋め込みます。

# 1. sudoを付与して安全にパスワードを取得し、置換を実行します
DB_PASS_TMP=$(sudo grep HEADSCALE_DB_PASS /srv/containers/headscale/.env | cut -d= -f2)
sudo sed -i "s/DB_PASS_PLACEHOLDER/${DB_PASS_TMP}/" /srv/containers/headscale/config/config.yaml
# 2. 閲覧権限の制限
sudo chmod 600 /srv/containers/headscale/config/config.yaml
# 3. SELinuxアクセス権の修復
sudo restorecon -Rv /srv/containers/headscale
# 4. 【最終検証】パスワードが埋め込まれたか確認
sudo grep db_pass /srv/containers/headscale/config/config.yaml
3-3

本格的3階層対応 ACLファイル(acl.hujson)の作成

【最重要バグ対策】strip_email_domain の仕様を理解する

config.yamlに strip_email_domain: true を設定すると(Phase 5で追加予定)、Authentik上のメールアドレスが user@home.local であっても、Headscale内部では@以降が削除された"user"単体として認識されます。そのためACLのgroup定義は必ずアカウント名単体("user")で記述します。
誤り例:"group:admin": ["user@home.local"] 正解例:"group:admin": ["user"]

sudo tee /srv/containers/headscale/config/acl.hujson << 'EOF'
{
  // ── グループ定義 ──────────────────────────────
  // 【重要】OIDC有効後はAuthentikのアカウント名(@より前)で記述する
  "groups": {
    "group:admin": ["user"],   // 管理者(全リソースにアクセス可能)
    "group:user":  [],         // 一般ユーザー(SERVER内のみアクセス可能)
    "group:guest": []          // ゲスト(将来用)
  },
  // ── タグ所有者定義 ─────────────────────────────
  // このタグを付与できるのはgroup:adminのメンバーのみ
  "tagOwners": {
    "tag:dmz":   ["group:admin"],  // Rocky Linux 9(DMZ)用
    "tag:lan":   ["group:admin"],  // Ubuntu / AlmaLinux(SERVER)用
    "tag:guest": ["group:admin"]   // ゲスト端末用(将来用)
  },
  // ── アクセス制御ルール ─────────────────────────
  "acls": [
    // 管理者はすべてのリソースに無制限でアクセス可能
    { "action": "accept", "src": ["group:admin"], "dst": ["*:*"] },
    // 一般ユーザーはtag:lanを持つデバイスにのみアクセス可能
    { "action": "accept", "src": ["group:user"],  "dst": ["tag:lan:*"] },
    // LANタグのデバイス同士は相互通信を許可
    { "action": "accept", "src": ["tag:lan"],     "dst": ["tag:lan:*"] }
    // ↑ tag:dmzからの許可ルールが存在しないため、
    //   DMZ → SERVERの通信は「暗黙の拒否」により完全遮断
  ]
}
EOF
3-4

Quadletファイル定義(/etc/containers/systemd/)

Quadlet とは?(--restart always との違い)

Quadlet は Podman 4.4+ で導入された「systemdのユニットファイルでコンテナを管理する仕組み」です。podman run --restart always はPodmanデーモンが再起動を管理しますが、Quadlet(systemd)はOSのinitシステムが再起動を管理します。依存関係(Requires=/After=)で起動順序を保証でき、journalctlでログが一元管理されます。

4-1. ネットワーク定義
# ★ 必ず最初に実行してNIC名を確認する
ip link show | grep -E "^[0-9]+:" | grep -v lo
# 出力例:
# 2: enp1s0: ...  ← これがNIC名(環境によって異なる)

sudo tee /etc/containers/systemd/local_lan.network << 'EOF'
[Network]
# ★ parent= の後ろは必ずご自身のNIC名に書き換えてください
# 確認方法: ip link show | grep -E "^[0-9]+:" | grep -v lo
Driver=macvlan
Options=parent=enp1s0
Subnet=192.168.2.0/24
Gateway=192.168.2.1
EOF
4-2. Pod定義
sudo tee /etc/containers/systemd/headscale.pod << 'EOF'
[Pod]
PodName=headscale-pod
Network=local_lan.network
IP=192.168.2.13
EOF
4-3. PostgreSQLコンテナ

二重定義に注意

EnvironmentFileで.envを読み込んでいる場合、同じ変数名をEnvironment=で再定義すると二重定義になります。このガイドではEnvironmentFileのみでDBパスワードを渡します。

sudo tee /etc/containers/systemd/headscale-db.container << 'EOF'
[Container]
Pod=headscale-pod.pod
ContainerName=headscale-db
Image=docker.io/library/postgres:<version>-alpine
EnvironmentFile=/srv/containers/headscale/.env
Environment=POSTGRES_DB=headscale
Environment=POSTGRES_USER=headscale
Environment=POSTGRES_PASSWORD=${HEADSCALE_DB_PASS}
Volume=/srv/containers/headscale/postgres:/var/lib/postgresql/data:Z
[Service]
Restart=always
EOF
4-4. Headscale本体
sudo tee /etc/containers/systemd/headscale.container << 'EOF'
[Unit]
Description=Headscale VPN Control Plane
# PostgreSQLが起動してからHeadscaleを起動する(依存関係)
Requires=headscale-db.service
After=headscale-db.service
[Container]
Pod=headscale-pod.pod
Image=docker.io/headscale/headscale:<version>
ContainerName=headscale
Volume=/srv/containers/headscale/config/config.yaml:/etc/headscale/config.yaml:ro,Z
Volume=/srv/containers/headscale/config/acl.hujson:/etc/headscale/acl.hujson:ro,Z
Volume=/srv/containers/headscale/data:/var/lib/headscale:Z
Exec=headscale serve
HealthCmd=headscale nodes list || exit 1
HealthInterval=30s
HealthRetries=3
[Service]
Restart=always
[Install]
WantedBy=multi-user.target
EOF
4-5. systemdに認識させて起動

【重要】Quadletで生成されたユニットはenableコマンドが使えません

startを使って起動します。OS再起動後の自動起動は/etc/containers/systemd/の設定ファイルからQuadletが自動的に処理するため、enableは不要です(詳細はトラブルシューティング記録・問題①)。

# Quadletファイルを変更したら必ずdaemon-reloadを実行する
sudo systemctl daemon-reload

# 起動(依存関係をAfter/Requiresで記述しているため、データベースも自動的に連動して起動します)
sudo systemctl start headscale-pod-pod.service
sleep 5
sudo systemctl start headscale-db.service
sleep 15
sudo systemctl start headscale.service
sleep 10

# ── 起動確認 ─────────────────────────────────
# headscale.serviceとheadscale-db.serviceがactive (running)であることを確認
sudo systemctl status headscale headscale-db
# ログで起動状況を確認(エラーがなければOK)
sudo journalctl -u headscale -f --no-pager | head -n 30

3. 正常性のテスト&検証コマンド

1. サービスの稼働ステータス確認

sudo systemctl status headscale

緑色で active (running) と表示されていれば、systemd管理下でのコンテナのバックグラウンド起動に成功しています。

2. コンテナ内部のヘルスチェック確認

コンテナ内部で定義した正常性診断のログを確認し、システムが正しく稼働しているかを確認します。

# ヘルスチェックの状態(healthyかどうか)をピンポイントで確認
sudo podman inspect --format '{{.State.Health.Status}}' headscale

画面に healthy と出力されれば、データベースとの接続、およびHeadscaleプロセスの応答がすべて正常に機能しています。

3. リアルタイムログ監視

# エラーなどが出ていないか、システム全体のログを確認
sudo journalctl -u headscale -n 50 -f

「An SQLite database path...」などの記述がなく、正常にデータベースと接続が完了していれば大成功です。

4. 初学者のためのミニ用語解説

用語説明
Macvlan
(マックブイラン)
1つの物理的なLANポートを、仮想的に複数のLANポートに切り分ける技術です。コンテナがあたかも個別の物理的なPCであるかのように、自宅ルーターから直接IPアドレス(192.168.2.13など)を取得できます。
Pod
(ポッド)
複数のコンテナを同じ「グループ」としてまとめる単位です。Pod内のコンテナはネットワークの空間を完全に共有するため、お互いを127.0.0.1(ローカルホスト)として超高速に呼び合うことができます。
Quadlet
(クアドレット)
Red Hatが提唱する最新のコンテナ管理手法です。従来の複雑なシェルスクリプトや手動コマンドを使わずに、設定ファイルを特定のフォルダに置くだけで、Linuxシステムが自動的にコンテナを「OSの常時監視サービス」に変換してくれます。
SELinuxコンテキスト
(:ro,Z / :Z)
Linuxに備わっている最高レベルのセキュリティ保護機能です。マウントする際に、:Zを指定することで「コンテナからの安全なアクセス」をSELinuxが許可し、さらに:roを付けることで「コンテナ内からの不意なデータの改ざん(書き込み)」を物理的に拒否します。
AlmaLinux 9 一覧へ戻る