ログイン機能を作らずに、管理画面をオンラインサーバーで運用する
管理画面本体は半日で作れたのに、ログイン機能の設計には2日かかる。そんな非対称は珍しくありません。利用者が自分と社内の数人に限られるなら、アプリへ認証を足す前に、そもそもインターネットから管理画面へ到達できない構成を検討できます。
認証を「アプリの外」に出す
認証を実装する仕事は、ログインフォームを作って終わりではありません。ユーザー管理、パスワードの保存と再設定、セッションの失効、総当たり攻撃への対策まで抱えることになります。実装後も脆弱性対応や退職者の権限削除が必要です。
一方、利用者が自分と数人だけなら、アプリには認証を書かず、SSH鍵や管理対象端末だけを入口にするほうが総コストと事故率を抑えられる場合があります。「URLを知られたら終わり」にしないため、推測しにくいURLではなく通信経路そのものを閉じるのがポイントです。
ただし、これは認証をなくすのではなく、SSHやVPNへ移す設計です。SSH鍵の配布・失効、サーバーアカウントの管理、端末の保護は引き続き必要です。
基本形は127.0.0.1 bindとSSHトンネル
まず、サーバー上の管理画面を0.0.0.0ではなく127.0.0.1だけで待ち受けさせます。ExpressとPythonの簡易サーバーなら、例えば次の指定です。
# Express: app.listen(3000, "127.0.0.1")
# Python(簡易確認用。本番の常駐サーバーにはしない)
python -m http.server 3000 --bind 127.0.0.1
このポートはファイアウォールで許可せず、Nginxなどのリバースプロキシにも登録しません。クラウドのセキュリティグループも含め、外部公開されていないことを確認します。
利用者のPCでは、SSHのローカルポートフォワードを開始します。
ssh -N -o ExitOnForwardFailure=yes \
-L 8080:127.0.0.1:3000 user@server
# 接続中にブラウザで http://localhost:8080 を開く
-LはPC側の8080を、SSH接続先から見た127.0.0.1:3000へ転送します。-Nはリモートコマンドを実行しない指定、ExitOnForwardFailureは転送用ポートを確保できなかったときに終了する指定です。PC側の待受アドレスを省略するとループバックだけで待ち受けるため、同じLANの別端末へ意図せず開放しません。
図:通信経路
> 利用者のブラウザ localhost:8080 → 暗号化されたSSHトンネル → サーバー内部 127.0.0.1:3000
>
> インターネットから管理画面のポートへ直接アクセスする経路は作りません。
この形では、SSH鍵を持ち、接続を許可された人だけが入口へ到達できます。可能なら管理画面用に権限を絞ったSSHアカウントを用意し、パスワードログインを無効化して、不要になった公開鍵は速やかに削除します。
常用するならTailscaleで共有する
毎回トンネルを張るのが面倒な場合や、管理対象の複数PCから使う場合はTailscaleも選択肢です。サーバーと各PCを同じtailnetへ参加させ、サーバー上で次を実行します。
tailscale serve --bg 3000
Tailscale Serveは、ローカルの3000番ポートをtailnet内へHTTPSで共有します。表示されたtailnet用URLを使えば、アプリをインターネットへ直接公開する必要はありません。バックグラウンド実行後はtailscale serve statusで設定を確認し、不要になったらtailscale serve resetで解除します。
名前が似たtailscale funnelは使いません。Funnelはtailnetの外、つまりインターネットからサービスへ到達させる機能で、今回の前提が崩れるためです。また、tailnetへ参加している全員へ無条件に開くのではなく、Tailscaleのアクセス制御で到達可能なユーザー、グループ、端末、ポートを必要最小限にします。端末の紛失時に無効化できる運用も先に決めておきます。
構成が決まったあとに待っている落とし穴
単一インスタンスで常駐させる
SSHを切ってもアプリが止まらないよう、手作業のバックグラウンド実行ではなくsystemdなどのサービスマネージャーへ任せます。
[Service]
User=admin-app
WorkingDirectory=/opt/admin-app/current
ExecStart=/usr/bin/node server.js
Restart=on-failure
専用の非特権ユーザーで実行し、起動方法を一つに固定します。systemdが再起動と多重起動の防止を担当するため、担当者ごとに別プロセスを立ち上げる事故を避けられます。実際のunitには環境変数の読み込み方法、停止時の挙動、権限の制限も追加してください。
データをコードと分離する
データファイルをリリースディレクトリへ置くと、デプロイ時の入れ替えで消すおそれがあります。コードとは別の永続ディレクトリへ保存し、世代を持つバックアップを取得します。バックアップは作るだけでなく、別環境へ復元できるかも定期的に確認します。
JSONストアの同時更新を見落とさない
JSONをload → modify → saveする実装では、テンポラリファイルへの書き込み後にatomic renameすれば、途中まで書かれたファイルへの置換は避けられます。しかし、2つの処理が同じ古い内容を読み、後から保存した一方がもう一方の更新を上書きする問題は防げません。
単一プロセス・単一ライターを維持できる間だけ使うのかを明文化し、並行更新が必要になる前にファイルロックやSQLiteなどのDBへ移行します。「ファイルが壊れない」と「更新を失わない」は別の保証です。
デスクトップ固有の機能を切り離す
デスクトップアプリから移植する場合、メニューバー常駐やOSのネイティブ通知はオンラインサーバーではそのまま使えません。データ操作はブラウザの管理画面へ寄せ、通知はメールや社内チャットなど別の経路へ分離します。画面だけ移して完成と考えず、OSが担当していた役割を洗い出します。
この構成をやめるべきとき
次のどれかに当てはまるなら、閉じた経路だけに頼らず、アプリ側の認証と監査ログを実装する段階です。
- 社外の人やクライアントにも操作させる
- 利用者が増え、「誰が何をしたか」を記録する必要がある
- 管理されていないPCからの利用を許可する
特に、SSHやtailnetへ入れる人を一人の利用者としてしか扱えない構成では、アプリ上の操作主体を識別できません。役割ごとの権限や操作履歴が必要なら、OIDCなど既存の認証基盤も利用して要件を満たします。
まとめ
「認証を書かない」は手抜きではありません。利用者が少なく、SSH鍵や管理対象端末を適切に運用でき、外部へ到達経路を作らないという条件とセットの設計です。まずは127.0.0.1とSSHトンネルで試し、利用頻度が上がったらTailscaleを検討する。条件が変わったら認証と監査へ移る、その境界まで先に決めておきましょう。
今回は以上です。