本ページの位置づけ
本ページはPhase 3(Headscale構築)の設計思想を3つの構造図で解説する資料です。実際の構築で使う最終版の手順・設定ファイルは「ゼロトラスト基盤構築ガイド(v5決定版)」に統合されており、本ページの手順もその内容と整合させています(NIC名の確認、Quadletの起動方法など、トラブルシューティング記録で判明した修正を反映済みです)。
1. 本システムを紐解く3つの構造図
図① 物理ホストとコンテナの接続(MacvlanとPod共有ネットワーク)
本構成では、物理NICを論理的に分割する「Macvlan」技術を使用し、コンテナ群にSERVERセグメントと同じセグメントの独立したIP(192.168.2.13)を直接割り当てます。
同一のIPアドレスをグループ内の全コンテナで共有
図② 同一Pod内におけるコンテナ間ループバック通信
Headscale本体からデータベース(PostgreSQL)にアクセスする際、接続先として 127.0.0.1(localhost)を指定します。通常コンテナはIPアドレスが異なりますが、同一の「Pod」に同居しているコンテナ同士は、ネットワーク名前空間(Network Namespace)を完全に共有するため、あたかも同一OSの中に同居しているかのように127.0.0.1を通じた超高速な内部通信が可能です。
図③ systemd(systemctl)によるコンテナの起動順序制御
データベースの起動が完了する前にHeadscaleが立ち上がると、データベース接続エラーによってHeadscaleコンテナが起動不全を起こし、再起動を繰り返す「接続ループ地獄」に陥ります。これを防ぐため、次世代サービス定義規格である「Quadlet」を使用し、「データベースが100%起動した後にのみHeadscaleを起動させる」という依存関係をシステムに強制します。
2. 構築手順
永続化ディレクトリの準備とセキュリティ初期化
コンテナが消滅したり再作成されたりしても、登録した端末データや暗号鍵が失われないよう、ホスト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
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階層対応 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
Quadletファイル定義(/etc/containers/systemd/)
Quadlet とは?(--restart always との違い)
Quadlet は Podman 4.4+ で導入された「systemdのユニットファイルでコンテナを管理する仕組み」です。podman run --restart always はPodmanデーモンが再起動を管理しますが、Quadlet(systemd)はOSのinitシステムが再起動を管理します。依存関係(Requires=/After=)で起動順序を保証でき、journalctlでログが一元管理されます。
# ★ 必ず最初に実行して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
sudo tee /etc/containers/systemd/headscale.pod << 'EOF'
[Pod]
PodName=headscale-pod
Network=local_lan.network
IP=192.168.2.13
EOF
二重定義に注意
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
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
【重要】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を付けることで「コンテナ内からの不意なデータの改ざん(書き込み)」を物理的に拒否します。 |