Tailscale のコントロールサーバー代替 OSS「Headscale」を Lightsail に構築してみた【月6ドル】
はじめに
製造ビジネステクノロジー部のふじい(大)です。
外出先から自宅のネットワーク内の PC や NAS にアクセスしたい、プライベートサブネット内の EC2 インスタンスにアクセスしたいといった場面ってありますよね。
それを実現するためには VPN を設定することになると思います。
その中でも Tailscale はインストールしてサインインするだけで無料で使うことができる気軽さから選んでいる人が多いのではないでしょうか。
ただ私が想定しているユースケースだと Tailscale の無料プランでは満たせないことが分かっていました。(特にユーザー数上限6名)
また有料プランはユーザー数での課金となり、複数名での利用は高額になりそうなこと、まずは一時的な検証目的だったため、有料プランを契約するほどでもなく代替案を探していました。
そこで見つけたのが Tailscale のコントロールサーバー代替 OSS の Headscale です。
自前でホスティングが必要ですが、最小構成の Amazon Lightsail 上に構築すれば月額6ドル程度に収まり、ユーザー数の上限といった制約も気にせずに使用できます。
Tailscale の Standard プランの「1ユーザーあたり月8ドル」と比較しても安く、ユーザー数が増えるほど Headscale 導入によるコストメリットを享受できます。
この記事では Headscale の Lightsail への構築、Headplane によるユーザー管理、その他運用面について触れていきたいと思います。
Tailscale とは
Tailscale は、WireGuard をベースにしたメッシュ型 VPN サービスです。
各端末にクライアントをインストールしてログインするだけで、tailnet と呼ばれる自分専用のネットワークが組まれ、端末同士が暗号化されたトンネルで直接通信できるようになります。
便利な機能としては以下があります。
- NAT 越え:ポート開放やグローバル IP なしで、自宅や外出先など別々のネットワークにある端末同士が直接つながる
- MagicDNS:IP アドレスを覚えなくても、端末名で名前解決できる
- ACL:どのユーザー・端末がどこに接続できるかをポリシーで制御できる
- Exit Node / Subnet Router:特定の端末をインターネットへの出口にしたり、tailnet 外のネットワークへの中継点にしたりできる
- マルチプラットフォーム:Windows / macOS / Linux / iOS / Android に公式クライアントがある
アーキテクチャ上の特徴として、端末間の実トラフィックは WireGuard の P2P で流れ、コントロールサーバーは鍵や設定の配布だけを担います。
NAT 越えがどうしてもできない環境のためには、DERP と呼ばれる中継サーバーが用意されています。
また Tailscale は SaaS として提供されており、無料の Personal プラン、有料の Standard/Premium/Enterprise プランがあります。
無料プランでは以下のように一部が制限されています。(2026年7月時点)
| 項目 | Personal(無料) |
|---|---|
| ユーザー数 | 最大 6 |
| ユーザーデバイス | 無制限 |
| ACL グループ | 最大 3 |
| タグ付きリソース | 50 個まで(超過分は 1 個あたり月 1 ドル) |
| 使えない機能 | SSO、ユーザー承認ワークフロー、ネットワークフローログ、JIT アクセス |
また、Personal プランは非商用向けと料金ページ下部の FAQ に記載があります。
This is a free plan and is only suitable for non-commercial use of Tailscale.
Headscale とは
Headscale は、Tailscale のコントロールサーバーをセルフホストするための OSS 実装です。
公式リポジトリでも以下のように紹介されています。
An open source, self-hosted implementation of the Tailscale control server
Tailscale のコントロールサーバーは鍵や設定の配布を担う部分です。
Headscale はこのコントロールサーバーと同じ API を実装しているため、公式の Tailscale クライアントをそのまま使えます。
Linux、macOS、Windows、iOS、Android のクライアントから、ログインサーバーの向き先を Headscale に変えるだけで接続できます。
端末間の実トラフィックが WireGuard の P2P で直接流れる点も Tailscale と変わりません。
違いは、コントロールサーバーを Tailscale の SaaS に任せるか、セルフホストするかだけです。
ライセンスは BSD 3-Clause で、セルフホストするので商用利用の制限もユーザー数による課金もありません。
これにより、「Personal プランは非商用向け」という前節の制約からも、人数が増えるほど費用が積み上がる有料プランの料金体系からも外れられます。
かかるのは、自前で用意するサーバーのリソース費用だけです。
コントロールサーバーとしての主要な機能は、一通り揃っています。
- MagicDNS、ACL、Exit Node、Subnet Router といった接続まわりの機能
- 非対話でノードを登録するための事前認証キー(pre-auth key)
- NAT 越えができないときに中継する DERP
一方で、SaaS 版の Tailscale と比べると割り切った部分もあります。
- 公式の Web 管理 UI がない。CLI 中心の設計で、GUI が必要なら Headplane などサードパーティの管理コンソールを導入する必要がある
- バックアップ、アップグレードは自分で管理する必要がある
- 一つのインスタンスで扱えるのは単一の tailnet で、個人利用や小規模な組織を想定している
- Funnel や Tailnet Lock など、一部の機能は非対応、または本家への追随に遅れがある
私のユースケースのように個人や少人数で使い、コストを抑えたい用途では、これらの割り切りはほとんど問題になりません。
GUI がない点は、Headplane を導入することで解消します。
Headplane とは
Headplane は、Headscale 用のサードパーティ製 Web 管理 UI です。
Tailscale 公式の管理コンソールの再現を目指しているプロジェクトで、以下のような操作をブラウザから行えます。
- ユーザーやノードの管理、事前認証キーの発行
- ノードの名前変更、オーナー変更、キーの有効期限、経路の承認
- ACL とタグの編集
- MagicDNS などの DNS 設定の編集
ライセンスは MIT で、Docker イメージが配布されています。
認証は Headscale の API キーによるログインのほか、OIDC にも対応しています。
今回は Headscale と同じインスタンスに Docker で同居させます。
今回の構成
Lightsail の単一インスタンスに Headscale 本体と Headplane を同居させ、CloudFormation で一式を管理する構成にしました。
- ホスティング:Lightsail の nano デュアルスタックプラン(月額 5 ドル、東京リージョン)
- OS:Ubuntu 24.04 LTS
- Headscale:DEB パッケージでインストール、状態は SQLite、TLS は内蔵の Let's Encrypt(autocert)
- DERP:Tailscale 公式の DERP サーバーを利用(自前では建てない)
- 管理 UI:Headplane を Docker で同居させ、公開せず localhost に限定
- IaC:CloudFormation(インスタンス、静的 IP、ファイアウォール、自動スナップショット、Route 53 の A レコードまで)
それぞれの選定理由を説明します。
サーバーが最小の nano プランで足りるのは、コントロールサーバーの仕事が鍵と設定の配布だけで、端末間の実トラフィックが通らないためです。
負荷もデータ転送量も小さく、静的 IP と 1 TB の転送量も料金に含まれるため、費用はほぼ固定になる想定です。
プランを IPv6 のみではなくデュアルスタックにしているのは、IPv4 しか使えないネットワークにいる端末からもコントロールサーバーに到達できるようにするためです。
Headscale の状態の保存は SQLite に、TLS 証明書は内蔵の Let's Encrypt 連携(autocert)に任せます。
個人や少人数の規模なら、外部のデータベースもリバースプロキシも必要ありません。
構成要素を減らした分だけ、運用で面倒を見る対象も減ります。
NAT 越えができないときの中継には、Tailscale が公開している DERP サーバーをそのまま使います。
Headscale には DERP サーバーを内蔵する機能もありますが、有効にすると中継トラフィックがこのインスタンスを通るようになり、負荷と転送量を抱え込むことになるため使いません。
Headplane は Docker コンテナとしてインスタンスに同居させます。
管理 UI はインターネットに公開せず、localhost にのみバインドして、使うときに手元から SSH ポートフォワードで開きます。
認証の仕組みを別途用意しなくても、SSH で入れる人だけが管理画面に触れる状態を作れます。
可用性についても触れておきます。
単一インスタンス構成であり、Headscale 自体も1つの tailnet に対して1インスタンスの運用しかサポートしていないため、コントロールサーバーの冗長化はできません。
そのかわり、コントロールサーバーが停止しても確立済みの端末間の通信は止まりません。
前述の通り、実トラフィックはコントロールサーバーを通らないためです。
DERP も Tailscale 公式のサーバーに任せているので、中継も影響を受けません。
停止中にできなくなるのは、新しい端末の登録、期限が切れたキーの再認証、ACL などの設定変更です。
個人や少人数の利用なら停止の影響はこの範囲に収まるので、スナップショットからの復旧で十分と割り切っています。
前提として、独自ドメインが必要です。
これは今回の構成に固有の前提ではなく、Headscale を運用すること自体の前提です。
Tailscale クライアントはコントロールサーバーに HTTPS で接続して証明書を検証するため、有効な証明書を発行できるドメイン名が構成によらず必要になります。
今回はこのドメインを名前解決と Let's Encrypt での証明書取得に使い、DNS が Route 53 であれば A レコードの作成まで CloudFormation に含められます。
CloudFormation テンプレート
作成したテンプレートの全文は以下の通りです。
CloudFormation テンプレートの解説
リソースとして定義しているのは、Lightsail インスタンス、静的 IP、Route 53 の A レコード(HostedZoneId を渡したときのみ作成)の3つだけです。
ファイアウォール(22/80/443 番ポート)と自動スナップショットは、インスタンスのプロパティとして設定しています。
ソフトウェアの導入と設定は、すべてインスタンスの UserData(起動スクリプト)で行っています。
- OS パッケージの更新(apt-get update / upgrade)
- スワップ 1 GB の確保(nano プランはメモリが 512 MB のため)
- Headscale の DEB パッケージのインストール
- config.yaml の書き換え(server_url、listen_addr、TLS、base_domain)
- ACL 初期ポリシーの配置(PolicyMode 指定時のみ)
headscale configtestでの検証- Headplane の Docker コンテナ起動(localhost:3000 にのみバインド)
ACL は、パラメータ PolicyMode で構成方法を選べるようにしています。
既定(none)では何も構成せず、Headscale の既定の挙動(全ノード間の通信をすべて許可)のままにします。
file または database を指定すると、全許可の初期ポリシー(挙動は none と同じ)を配置した上で有効化し、これを雛形として段階的に絞っていけます。
database にすると、後述の Headplane の GUI からも編集できます。
ユーザーの作成や事前認証キーの発行といった実行時データの投入だけは、デプロイ後に手作業で行います。
必要なコマンドはスタックの Outputs に出力されるようにしてあり、後の節で順に実行していきます。
なお、このテンプレートは一度デプロイするためだけの、イミュータブルな作りです。
ソフトウェアの導入と設定を担う UserData は初回起動時にしか実行されず、スタックを更新しても既存のインスタンスには再適用されません。
OS イメージやプランといった基本プロパティの変更も、インスタンスの作り直しになります。
構成を変えたいときはスタックを作り直し、稼働中のサーバーに対しては後述するアップグレードなどのインプレース作業で対応します。
Tips: UserData を POSIX sh で書いている理由
Lightsail は UserData を自前の #!/bin/sh ラッパーに連結し、dash で実行します。
独自スクリプトの先頭に書いた shebang はラッパーの後ろに連結されて 1 行目にはならず無視されるため、#!/bin/bash と書いても bash では実行されません。
dash は set -o pipefail などの bash 専用構文に対応しておらず、これらを書くとその行でエラー終了します。
テンプレートの UserData を POSIX sh 互換で書き、bash 専用構文である set -o pipefail、[[ ]]、配列を避けているのはこのためです。
また UserData の失敗はスタックのステータスに現れません。
CloudFormation はインスタンスの作成完了までしか待たず、起動スクリプトの成否を関知しないためです。
デプロイは「完了」するのにサーバーには何も構築されない、という分かりにくい状態になります。
エラーは /var/log/headscale-bootstrap.log に残ります。
(このテンプレートでは UserData の先頭で出力をこのファイルにリダイレクトしています)
UserData を改造して動かないときは、まずこのログを確認するとよいでしょう。
デプロイする
パラメータを渡してデプロイします。
IAM リソースを含まないテンプレートなので、--capabilities の指定は不要です。
aws cloudformation deploy \
--template-file lightsail-headscale.cfn.yaml \
--stack-name headscale \
--region ap-northeast-1 \
--parameter-overrides \
DomainName=headscale.example.com \
BaseDomain=tailnet.example.com \
AvailabilityZone=ap-northeast-1a \
HostedZoneId=Z0123456789ABCDEFGHIJ \
SshAllowedCidr=xxx.xxx.xxx.xxx/32
- DomainName:コントロールサーバーの公開 FQDN。A レコードと TLS 証明書がこの名前で作られる
- BaseDomain:MagicDNS の接尾辞。tailnet 内の端末名の後ろに付く
- AvailabilityZone:デプロイ先リージョンの AZ
- HostedZoneId:Route 53 のホストゾーン ID。省略した場合は A レコードが作成されないため、Outputs に出力される静的 IP へ手動で向ける
- SshAllowedCidr:SSH(22 番ポート)を許可する送信元。デフォルト値はないので必ず指定する。管理端末の IP(/32)に絞ることを推奨
デプロイは3分くらいで完了します。
疎通確認
デプロイが完了したら、名前解決と HTTPS の疎通を確認します。
dig +short headscale.example.com # 静的 IP が返る
curl -sS https://headscale.example.com/health
# => {"status":"pass"}
{"status":"pass"} が返れば、公開インターネットから HTTPS で到達でき、Let's Encrypt の証明書取得(autocert)も済んでいます。
証明書は、名前解決が通った後の最初のアクセス時に発行されます。
SSH の設定
以降の節ではサーバーに SSH して作業するので、先に鍵の設定を済ませておきます。
テンプレートの KeyPairName パラメータを省略した場合、インスタンスにはリージョンのデフォルトキーペアが設定されています。
Lightsail コンソールでインスタンスのページを開き、「Connect」タブの「Use your own SSH client」にある「Download default key」からダウンロードします。

ダウンロードした鍵を ~/.ssh に移し、権限を絞ります。
mv ~/Downloads/LightsailDefaultKey-ap-northeast-1.pem ~/.ssh/
chmod 400 ~/.ssh/LightsailDefaultKey-ap-northeast-1.pem
~/.ssh/config にエントリを追加しておくと、以降の接続が楽になります。
接続先の静的 IP は、スタックの Outputs(StaticIp)で確認できます。
Host headscale
HostName <静的 IP>
User ubuntu
IdentityFile ~/.ssh/LightsailDefaultKey-ap-northeast-1.pem
IdentitiesOnly yes
IdentitiesOnly yes を付けているのは、ssh-agent に登録された鍵が多いと、指定した鍵を試す前にサーバー側の試行回数の上限に達して Too many authentication failures で切断されることがあるためです。
ssh headscale でログインできれば準備完了です。
もしログインに失敗する場合は、Lightsail の SSH ポートを許可しているネットワークルールの IP アドレスと PC 側の IP アドレスが一致するか確認しましょう。
また、初回ログイン時に *** System restart required *** と表示されることがあります。
UserData の apt-get upgrade でカーネルなどが更新され、再起動するまで新しいカーネルが有効にならないためです。
Headscale も Headplane も再起動後には自動で復帰するので、クライアントを繋ぐ前のこの時点で再起動を済ませておきます。
sudo reboot
Tailscale クライアントから接続する
Headscale のユーザーは、ノードを所有する単位です。
個人が使う端末は、その所有者のユーザー(例:alice)の配下に登録します。
複数人で共有するサーバーは特定の個人に紐づかないので、ユーザーには属させず、タグ(例:tag:server)を付けて登録します。
こうしておくと、後から nodes list や ACL で見たときに、個人の端末は所有者で、共有サーバーはタグで区別できます。
ここでは、個人の PC、個人の Android、共有のサーバーの三通りの登録手順と、サーバーを操作できないユーザーの端末を追加する場合の流れを示します。
個人所有の PC を対話的に登録する
最初に、その端末の所有者のユーザーをサーバー側で作成します。
headscale CLI は稼働中のデーモンと unix ソケットで通信するため、sudo が必要です。
# ssh headscale で入って実行する
sudo headscale users create alice
次に、PC に公式クライアントをインストールします。
Windows と macOS のクライアントは、Tailscale の公式サイトや各 OS のストアから入手できます。
クライアントを入れたら、ログインサーバーを Headscale に向けてログインします。
GUI からは Account Settings 画面の Add Account... ボタン横のアイコンをクリックし、https://headscale.example.com を入力します。
コマンドライン版のクライアントを使う場合は、tailscale login --login-server https://headscale.example.com で指定できます。

いずれの方法でも、ログインを実行するとブラウザが開き、登録に使う Auth ID が表示されます。

画面の案内に従い、その Auth ID を渡してサーバー側で登録すると、ノードが tailnet に加わります。
# ssh headscale で入って実行する
sudo headscale auth register --auth-id <ブラウザに表示された Auth ID> --user alice
ここでも sudo が必要で、付けないと permission denied でソケットに繋がりません。
個人所有の Android を登録する
Android では、Google Play で配布されている公式アプリを使います。
アプリにログインサーバーを変更する設定があるので、そこに Headscale の URL を指定します。
執筆時点のアプリでは、アプリ右上の設定メニューを開いて Accounts を選び、さらに右上の三点メニューから Use an alternate server を選んで、https://headscale.example.com を入力します。


以降は PC と同じくサーバー側での登録です。
画面の案内に従って登録を進め、表示された Auth ID を渡してサーバー側で headscale auth register を実行すると、ノードが登録されます。
この Android も個人の端末なので、--user にはその所有者の alice を指定します。
手元に iOS がないため Android の記載のみですが、iOS でも同様の手順でいけるはずです。
共有のサーバーを事前認証キーで登録する
共有のサーバーには、ブラウザでの対話操作を挟まない事前認証キーでの登録が向いています。
サーバーには操作するためのブラウザがないことが多く、キーを渡すだけで登録が完結するためです。
このサーバーは特定の個人に紐づかないので、ユーザーを作らず、タグを付けた事前認証キーを発行します。
# ssh headscale で入って実行する
sudo headscale preauthkeys create --tags tag:server --reusable --expiration 24h
タグは tag: で始まる小文字という書式さえ満たせばよく(複数付けるならカンマ区切り)、ACL ポリシーや tagOwners の宣言は要りません。
このテンプレートの既定(PolicyMode=none)のままでも登録できます。
このキーで登録したノードはどのユーザーにも属さず、指定したタグがそのまま付与されます。
タグの付いたノードはノードキーの有効期限が無効になり、繋ぎっぱなしのサーバーでも期限切れによる再認証が起きません(--expiration はこれとは別で、キーを登録に使える期間の指定です)。
--reusable を付けたキーは、有効期限内であれば複数の端末で使い回せます。
同じ役割のサーバーを何台か登録するときや、キーを構成管理ツールに書いておいて使い回すときに便利です。
一台を一度だけ登録すれば十分なら、--reusable を外して使い捨てのキーにします。
発行したキーを控えたら、サーバーに公式クライアントをインストールし、キーを渡して登録します。
tailscale up は Linux ではネットワークインターフェースの設定に管理者権限が要るため、sudo を付けます。
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up --login-server https://headscale.example.com --auth-key <発行したキー>
サーバーを操作できないユーザーの端末を事前認証キーで登録する
ここまでの手順は、サーバーを操作できる管理者が自分で登録を完結する前提でした。
サーバーを操作できないユーザーの端末を対話登録で追加するには、本人が Auth ID を管理者に伝えて承認してもらう往復が必要です。
事前認証キーを使うと、この往復をなくせます。
管理者がキーを用意して渡せば、あとは本人の操作だけで登録が完結するためです。
管理者側では、本人のユーザーを作成してキーを発行します。
本人のユーザーで発行するのは、登録された端末の所有者を正しく付けるためです。
# ssh headscale で入って実行する(後述の Headplane の GUI でも可)
sudo headscale users create bob
sudo headscale users list # 表示された数値 ID を控える
sudo headscale preauthkeys create --user <ID> --expiration 1h
共有サーバーのときと違い、--reusable は付けず有効期限も短くしておきます。
使い捨てで短命なキーにしておけば、渡す途中で漏えいしても影響を一台分と短時間に絞れるためです。
発行したキーは、安全な経路で本人に渡します。
本人側の操作は、クライアントをインストールしてキーを渡すだけです。
sudo tailscale up --login-server https://headscale.example.com --auth-key <受け取ったキー>
なお、Android アプリでの事前認証キーによる登録は公式ドキュメントに手順がなく、Android の端末は対話登録が基本です。
その場合は本人が表示された Auth ID を管理者に伝え、管理者が headscale auth register で承認します。
登録を確認する
サーバー側で、登録されたノードの一覧を確認します。
sudo headscale nodes list
個人の端末は、所有者のユーザー(alice や bob)の配下に並びます。
タグで登録した共有サーバーはどのユーザーにも属さないため、User 列が tagged-devices となり、Tags 列に付けたタグが表示されます。
これらが一覧に出ていれば登録完了です。
外出先の端末から自宅のサーバーにアクセスする
登録が済めば、tailnet に参加した端末同士は同じネットワークにいなくても直接つながります。
ここでは、自宅の LAN に置いた共有サーバー(ノード名 nas)へ、外出先でモバイル回線につないだ PC からアクセスしてみます。
同じ LAN におらず、ルーターのポート開放もしていないのに届く、というのがここで確かめたい点です。
外出先の端末で tailscale status を見ると、nas を含む tailnet の各ノードが、それぞれの tailnet 内アドレス(100.64.0.0/10 の 100.x.y.z)とともに一覧に出ています。
届くかどうかと経路は、tailscale ping で確かめられます。
# 外出先の端末で実行する
tailscale ping nas
# => pong from nas (100.x.y.z) via <相手のグローバル IP:ポート> in 23ms
via に相手のグローバル IP が出ていれば、実トラフィックは P2P で直接流れています。
NAT 越えができないときは via DERP(tok) のような中継表示になりますが、どちらの場合もコントロールサーバー(Lightsail のインスタンス)は経由しません。
宛先には MagicDNS のノード名(<ノード名>.<BaseDomain>)をそのまま使えます。
名前解決はクライアント側の追加設定なしに効くので、たとえば SSH なら次のように接続します。
ssh nas.tailnet.example.com
サーバー OS 側のファイアウォールで着信を絞っている場合は、tailscale0 インターフェースか 100.64.0.0/10 からの着信を許可しておきましょう。
Headplane でユーザー管理する
ユーザーの追加やキーの発行のたびに SSH してコマンドを打つのは手間です。
そこで Headplane を使います。
テンプレートの UserData で導入済み(InstallHeadplane の既定値が true)なので、追加の構築作業はありません。
管理 UI は localhost:3000 にしかバインドしていないため、手元から SSH ポートフォワードでアクセスします。
同等のコマンドが Outputs の HeadplaneTunnel にも出力されています。
ssh -L 3000:localhost:3000 headscale
トンネルを張ったまま、ブラウザで以下を開きます。
http://localhost:3000/admin/
開くと、Headscale の API キーを入力するログイン画面が表示されます。
キーはこの場で発行します。ログイン画面にも headscale apikeys create で生成するよう案内が表示されています。
# ssh headscale で入って実行する
sudo headscale apikeys create
キーは作成時に一度しか表示されないので、そのまま貼り付けてログインします。
有効期限は既定で 90 日で、--expiration で変えられます。
ログイン後のセッションもこのキーの残り期限まで有効で、切れたら新しいキーを発行して再ログインします。
発行したキーは Headscale 側に残り続けるため、不要になったものは sudo headscale apikeys list で確認し、sudo headscale apikeys expire --prefix <プレフィックス> で失効させます。
なお、API キーでのログインは Headscale への管理者アクセスそのものです。
キーの取り扱いには注意してください。
ダッシュボードが開けば、以降はユーザーの追加、事前認証キーの発行、ノードの管理をブラウザで完結できます。
PolicyMode を database にしている場合は、ACL の編集も GUI から行えます。
ユーザーと事前認証キーを GUI で作成する
先に CLI で行ったユーザー作成と事前認証キーの発行は、この画面からも実行できます。
画面のラベルは執筆時点の Headplane(0.7.0)のもので、バージョンによって名称が変わることがあります。
名称が違うときは、操作の目的から読み替えてください。
まずユーザーを作成します。
ナビゲーションから Users を開くと、登録済みのユーザー一覧が表示されます。
この画面で「Add user」を押すと、ユーザー作成のダイアログ(Create a Headscale user)が開きます。
入力欄は「Username」が必須で、「Display Name」と「Email」は任意です。
CLI の headscale users create alice と同じく、ここで決めるのはユーザー名だけで足ります。
作成すると、一覧に新しいユーザーが追加されます。

次に、作成したユーザー向けの事前認証キーを発行します。
ナビゲーションの Settings から Manage Auth Keys のリンクを開くか、Machines から「Add Device」ボタンを押して表示される「Generate Pre-auth Key」を選択し、Pre-Auth Keys 画面を開きます。
「Create pre-auth key」を押すと、発行ダイアログ(Generate auth key)が表示されます。
キーの所有者は「User」で選びます。
CLI では --user に渡す数値 ID を headscale users list で調べましたが、GUI では一覧から選ぶだけなので ID を調べる必要はありません。
「Reusable」を有効にすると、CLI の --reusable と同じく 1 つのキーを複数の端末で使い回せます。
有効期限は「Key Expiration」で設定します。
このフォームは日数での指定で、既定値は 90 日です。
CLI では --expiration 24h と時間単位で指定したので、ここは日数に読み替える点だけ異なります。
切断時に自動で削除される端末向けにするなら「Ephemeral」を、タグを付けるなら「ACL Tags」を使います。
「ACL Tags」は共有サーバーの節で使った --tags、「Ephemeral」はここまで触れていない --ephemeral に当たります。

内容を確認して発行すると、生成されたキーが一度だけ表示されます。
画面にも「Copy this key now.」と出るとおり、閉じると全体を再表示できないため、その場でコピーします。

あとは CLI で発行したキーと同じく、端末側で tailscale up --auth-key にこのキーを渡して接続します。
ここまでで、ユーザーの作成から事前認証キーの発行までを SSH せずに完結できます。
運用面
最後に、コスト、バックアップ、アップグレードについてまとめます。
コスト
無料枠を除いた月額の内訳は以下の通りです。(東京リージョン、2026年7月時点)
| 項目 | 課金の基準 | 月額(ドル) |
|---|---|---|
| Lightsail nano デュアルスタック | 定額(1 TB 転送込み) | 5.00 |
| 静的 IP | バンドル込み、追加課金なし | 0.00 |
| Route 53 ホストゾーン | 0.50/ゾーン | 0.50 |
| 自動スナップショット | 0.05/GB 月(増分) | ~0.50 |
| データ転送(送信超過分) | 1 TB 超過分のみ 0.14/GB | ~0.00 |
合計はおよそ月6ドルで、大半が固定費です。
データ転送がほとんど発生しないのは、公式 DERP を使う構成では端末間の実トラフィックがこのサーバーを通らないためです。
内蔵 DERP に切り替えると中継トラフィックがサーバーを通過するようになるため、1 TB を超えた送信分に課金される点には注意してください。
ちなみに EC2 で同等の構成を組むと、無料枠を除く最小クラスの t4g.nano でも月およそ 3.9 ドルかかり、そこにパブリック IPv4 アドレスの約 3.65 ドルと EBS 代が別建てで乗り、送信データ転送も従量課金になります。
データ転送を除いた固定費だけで、月 8 ドルを超えます。
IPv4 と 1 TB の転送をバンドルしている Lightsail は、今回のような小規模運用だと総額が読みやすいです。
バックアップ
バックアップは、テンプレートで有効化した自動スナップショットに任せています。
毎日1回ディスク全体のスナップショットが取られ、Lightsail の既定で直近7世代が保持されます。
テンプレートで指定しているのは取得時刻(18:00 UTC)だけで、保持世代数に設定項目はありません。
Headscale の状態は単一の SQLite ファイルなので、ディスクのスナップショットで丸ごと戻せます。
復旧時は、スナップショットから新しいインスタンスを作成して静的 IP を付け替えます。
アップグレード
Headscale と Headplane のアップグレードは自分で管理する必要があります。
テンプレートの HeadscaleVersion と HeadplaneImage は初回起動時の UserData でしか使われないため、スタックの更新ではなく、SSH してのインプレース更新になります。
Headscale の公式ドキュメントに書かれているアップグレードの原則は以下の通りです。
- リリースノートを確認する
- マイナーバージョンを飛ばさず、順に上げる(例:0.27 → 0.28 → 0.29)
- 更新の前に停止し、バックアップを取る
起動時に DB マイグレーションが自動実行され、ダウングレードは想定されていません。
戻す手段は実質スナップショットからの復元だけなので、直前に手動スナップショットを取ってからアップグレード作業を行いましょう。
# 手元から: 直前のバックアップを取る
aws lightsail create-instance-snapshot \
--instance-name headscale \
--instance-snapshot-name headscale-pre-upgrade \
--region ap-northeast-1
Headscale 本体は DEB でインストールしているので、更新も DEB で行います。
# サーバー側 (ssh headscale) で実行する
sudo systemctl stop headscale
ARCH=$(dpkg --print-architecture)
wget -O /tmp/headscale.deb \
"https://github.com/juanfont/headscale/releases/download/v<新バージョン>/headscale_<新バージョン>_linux_${ARCH}.deb"
sudo apt-get install -y /tmp/headscale.deb
sudo headscale configtest # 新版で設定キーが変わっていればここで検出できる
sudo systemctl start headscale
curl -sS https://headscale.example.com/health
Headplane は Docker コンテナなので、新しいイメージを pull してコンテナを作り直します。
設定とデータはホスト側にマウントしてあるため、コンテナを消しても失われません。
ただし --log-opt(ログの肥大化を防ぐローテーション設定)のような起動オプションはマウントでは引き継がれないため、初回起動時と同じものを付け直します。
sudo docker pull ghcr.io/tale/headplane:latest
sudo docker rm -f headplane
sudo docker run -d --name headplane --restart unless-stopped \
--log-opt max-size=10m --log-opt max-file=3 \
-p 127.0.0.1:3000:3000 \
-v /etc/headplane/config.yaml:/etc/headplane/config.yaml:ro \
-v /var/lib/headplane:/var/lib/headplane \
-v /etc/headscale/config.yaml:/etc/headscale/config.yaml:ro \
ghcr.io/tale/headplane:latest
おわりに
Lightsail 上にセルフホストの Headscale を建てて、Headplane での GUI 管理までを CloudFormation で再現できる形にしました。
費用は月6ドル前後で、ユーザー数が増えても変わりません。
Tailscale の無料プランで足りているうちはそれが一番手軽ですが、ユーザー数の上限や商用利用の制約に引っかかったときには、この構成が現実的な選択肢になると思っています。
同じ構成を検討している方の参考になれば幸いです。




