エージェントを手元の一台だけで動かすなら、通信はほとんど設計になりません。プロセスは同じマシンにあり、操作する人もそこにいます。困り始めるのは、作業の置き場が別のマシンで、そのマシンが NAT や組織の境界の向こうにあり、外からポートを開けないときです。そのマシンのネットワークは、こちらが管理できません。アドレスを控えても、そのアドレスへ制御プレーンから届く保証はありません。

この構成では、そのマシンを常時の管理対象にしません。人が入るのは導入のときだけで、その後の生存と身元は、マシン自身が外へ張る接続で保ちます。対話的なデータの通り道は、それとは別の、人がその場にいるときだけの経路に残します。

課題

知りたいことは単純です。届かせる手段がありません。

制御プレーンは、そのインストールが今生きているか、作業を受けられる状態か、意図して止めたのか、それとも消えたのかを区別したいのです。作業の宛先を一台に固定したなら、沈黙しているあいだはそこで待たせます。別のマシンへ黙って移すと、置いてある作業の文脈が分かれます。

同時に、作業そのもののバイト列はマシンの上にあります。ファイルの読み書きや、実行中のプロセスの入出力です。制御プレーンがそれを代理で運ぶと、プレーンがデータ面になります。接続を特定のインスタンスに固定できない API では、開いた流れと、作業が生まれたリクエストが同じ場所に着地しません。

操作する側の UI は、人が閉じます。閉じた瞬間に常駐が死ぬと、常駐にした意味がありません。ラップトップを畳んだ、というだけで「マシンが無い」になってはいけません。

同じ筐体を複数人が使うこともあります。SSH の宛先は筐体に対するもので、インストールの身元ではありません。到達できたことと、どのアカウントの常駐に繋がったかは、別の事実です。

秘密の置き方も課題になります。導入が成功した状態が、プロセス一覧やサービス定義を読める人にとっての資格情報になってはいけません。

身元を自己申告の文字列で済ませると、別の問題が残ります。ボディに「自分はこのマシンだ」と書くだけでは検証できません。あるマシン宛の作業を、同じ人の別のマシンが取れます。一台を失効させても、共有している鍵が残ります。

制約

この構成が置いた制約は、次のとおりです。

導入が終わったあと、エージェントは待受を持ちません。制御プレーンとの通信は、マシンからの外向きだけにします。制御プレーンは、マシンのネットワークアドレスを保存しません。ループバック以外への待受は拒否します。

導入の経路は、人がすでに持っている SSH に限ります。そのセッションは短いです。人が端末の前にはいますが、その端末に人のセッションが無い場合は、短い承認コードを人がブラウザで認めます。いずれにしても、新しいマシンを認める権限は人のセッションにだけ置き、自動で動く主体には持たせません。

同じマシンの上での制御は、UNIX ドメインソケットに置きます。ソケットはホストの外へ出ません。リクエストごとに、ローカルなトークンを要求します。

インストールの身元は、そのインストールに発行した資格情報で証明します。手元に残す識別子は、再起動のあとで自分の行を探すためのもので、認可には使いません。資格情報は、プロセスの引数、サービス定義、サービスマネージャがプロセスへ渡す環境には置きません。導入時は SSH の標準入力で渡し、所有者だけが読めるファイルへ、中身を書く前に権限を絞ってから原子的に置き換えます。

UI を閉じてもデーモンは残ります。止めるのは、明示の停止、OS からの停止シグナル、マシンそのものの停止だけです。明示の停止はラッチを残し、UI の再起動やログインのやり直しは、その停止を取り消しません。

人があとから対話するデータ面は、その人が張った SSH で、先の UNIX ソケットへ転送します。制御プレーンは、ファイルの中身や実行の記録を通しません。

採らなかった方式

インバウンドを開ける

ポートを公開する、逆向きのトンネルを常時張って制御プレーンがダイヤルする、待受のアドレスを台帳に書く、という方式です。対象にしているマシンは、まさにそれができない側です。NAT の対応表は、内側から開けたものだけがしばらく生きます。外側から新しい流れを始めても、届く相手がいません。

仮に開けたとしても、制御プレーンがアドレスと待受を運用し続けることになります。マッピングは切れ、アドレスは変わり、待受は露出します。この構成では、エージェントの待受を持たず、アドレスも持ちません。

常時の VPN

全マシンを一つのネットワークに入れると、SSH も待受も「届く」ように見えます。届くようにしているのは、そのネットワークをこちらが管理しているからです。管理できないマシンに対して、ネットワークの管理を前提にしています。

残る問題もあります。トンネルのアドレスは、インストールの身元ではありません。VPN の資格情報と、マシンの資格情報を二重に回すことになります。ラップトップが眠ると経路が落ち、エージェントが死んだように見えます。見たい事実は、そのインストールが生きていて、誰のもので、作業を受けられるかです。経路が生きていることは、その代わりになりません。

データ面まで SSH を使い続ける

SSH は、導入と、人がいるあいだの対話には合います。生存確認と作業の受け取りまで同じセッションに載せると、セッションを握っている側が死んだ瞬間に、マシンが不在になります。UI を閉じた、ラップトップを閉じた、踏み台が切れた、というだけで常駐が消えます。

対話的なバイト列は、人が見ているあいだだけで足ります。生存はそれより長いです。SSH をデータ面の経路として残すことと、SSH をマシンの存在条件にすることは、別の決定です。この構成が採らなかったのは、後者です。

生存確認にデータ面を載せる

短い外向きの確認に、ファイルや実行の入出力を載せる方式です。一回の転送が遅れると、マシンが落ちたように見えます。確認の処理は、ディスクや大きな応答を待ってはいけません。

制御プレーンが作業コピーの通り道にもなります。プレーンは、他人のファイルや実行の記録を持つ場所ではありません。接続を特定のインスタンスに固定できない API では、開いたソケットと、作業が生まれたリクエストが同じプロセスに着地しません。プッシュのために接続を固定する経路は、この構成の前提にしませんでした。

生存と作業の取得を、一つの長い保留で兼ねる案もありました。保留が切れたときと、マシンが死んだときが、外からは同じ沈黙になります。静かなマシンが接続を握り続けるコストも残ります。生存は短い確認のままにし、作業の取得は別の短い外向きリクエストにしました。

遠いディスクを手元にマウントする

手元のツールに、先のファイルシステムを見せる方式です。エージェントが動く場所は先のマシンです。マウントは先にプロセスを生みません。遅延の大きい経路では、状態を細かく見る道具が先に壊れます。端末も、実行の継続も、この方式では届きません。

人の資格情報をマシン間で共有する

人の鍵を各マシンが持ち、ボディの識別子で「どのマシンか」を名乗る方式です。識別子は自己申告なので、検証になりません。一台を失効させても同じ鍵が残り、あるマシン宛の作業を別のマシンが取れます。資格情報はインストールに結び、人のセッションとは分けます。

採用した通信の形

通信は三層に分けました。導入のときだけの SSH、その後ずっと続くマシン発の確認、人がいるときだけ戻ってくるデータ面、です。

下の三つの図は、経路ごとに、何がどの向きに流れ、何が通らないかを描いています。

導入だけ、人が持つ SSH

矢印は、人からマシンへ向かいます。向かうのは、導入のときだけです。SSH が運ぶのは、成果物と資格情報です。資格情報は標準入力に載ります。制御プレーンには、この経路の矢印がありません。向こうからダイヤルしません。資格情報は、引数には流れません。

前提は、人がその OS アカウントへ SSH できることです。制御プレーンがその経路を代わりに持つわけではありません。

初回は、ホスト鍵の指紋を人に見せます。確認の前に走らせるのは、実害のないコマンドだけです。資格情報も、実行の入力も、この確認の前には送りません。確認のあとは厳密に照合し、鍵が変わったら接続を止めます。

成果物は、その検証済みの接続で置きます。署名を確認してから、サービスマネージャの対象にします。後からの更新も、人が接続したときの事前確認で行い、実行中の作業があるあいだは延期します。通常の利用者向けコマンドを更新しても、この常駐が指している実体は変わりません。

資格情報は標準入力で渡します。引数にすると、プロセス一覧に残ります。サービス定義や、サービスマネージャが読む環境に書くと、定義を読める範囲が資格情報の範囲になります。デーモンは、所有者だけが読める権限を先に付けた一時ファイルへ書き、同期してから置き換えます。空のファイルが途中で見えることはあっても、中途半端な中身が資格情報の名前で残ることは避けます。

置いた直後に、その資格情報で外向きの確認を一回行います。制御プレーンが返した行が、今選んでいるマシンと一致することを見てから、実行の制御を開きます。揃えるものは三つです。SSH が認証した利用者、固定したホスト鍵、資格情報に結び付いた制御プレーン上の行です。トンネルの先が返した行と、人が選んだ行が違うなら、その接続では実行を開きません。

手元に UI が無い導入は、短いコードを人がブラウザで承認します。交換の記録に、資格情報の実体は置きません。承認前の自己申告、名前やアカウントや識別子は、承認する人がどのマシンかを見分ける表示です。証明ではありません。承認後の受け取りは一度だけで、期限切れも拒否も使用済みも、外からは同じ失敗に見えます。どれが起きたかを分けると、コードを見ているだけの相手に、内部の状態が漏れます。

その後は、マシンからの接続

導入のあと、マシンは制御プレーンへ、外向きの矢印を二つ開きます。確認が運ぶのは、生存と身元です。作業の取得は、もう一本の矢印で、確認とは分かれています。ファイルの中身と、実行の入出力は、確認へ向かう矢印を持ちません。制御プレーンからマシンへ戻る矢印はありません。ダイヤルしません。

サービスマネージャがデーモンを持ちます。UI の終了は、停止を呼びません。次に UI を開いたときは、動いているデーモンをそのまま使います。明示の停止だけがラッチを残し、UI も OS のログインも、それを取り消して起動し直しません。

ログアウトのあとにもユーザーサービスが残る設定が要る環境があります。それが無いと、デーモンはログオフで死にます。この欠落は、不在とは別に、「起きても、このままでは作業を受けられない」側の事実として返します。ログインに紐づく常駐と、ログイン無しの常駐は、壊れ方が違います。同じ「止まっている」にまとめると、どちらを直せばよいかが消えます。

制御 API は、そのアカウントの UNIX ソケットだけです。トークンはリクエストごとに要ります。転送の近端を UNIX ソケットにできず、ループバックの TCP にせざるを得ない環境でも、境界はトークン側に残します。ポートに届いただけのプロセスは、認証の失敗しか得ません。トークンは操作側のメモリに置き、そちらにはディスクへ書きません。

生存確認は、外向きの HTTPS です。間隔はサーバが毎回返します。クライアントに焼き込むと、間隔を変えたいマシンにだけ、新しいバイナリが届きません。下限は数十秒です。資格情報を受け取ったとき、準備状態が変わったとき、停止する直前には、間隔を待たずに打ちます。探査がばたついて熱ループにならないよう、連続の下限だけは置きます。

停止の直前の一回が、「止めた」と「消えた」を分けます。この一回は、すでに取り消された処理の文脈を継ぎません。短い独自の期限で送ります。理由は、人の操作、更新、再起動、引き継ぎ、シグナル、失効のように、決まった語だけを載せます。

確認のボディに書く名前、版、準備状態、ライフサイクルは、表示です。権限は生みません。認可は資格情報だけが持ちます。識別子を知っていても、既に資格情報があるインストールの二枚目は発行しません。再発行は、今の資格情報によるローテーションか、マシンの削除です。削除は識別子を再び使えるようにします。これを欠くと、秘密を失ったマシンを入れ直せません。未使用の識別子を名乗って資格情報を得るには、そのマシン上の識別子を読めている必要があります。識別子はどこにも公開しません。

ローテーションは、新しい世代を出したうえで、古い世代をすぐ捨てません。新しい値を受け取ってから保存するまでの間に落ちても、古い鍵で確認を続けられるようにするためです。古い世代を切るのは、新しい鍵が一度成功したときです。成功が、届いた証拠になります。古い鍵から次の世代を始めさせると、控えてあった古い鍵が別の世代列を始められます。ローテーションを始めてよいのは、今有効な世代だけです。

失効と、ローテーションの遅れは、応答で区別します。マシンが削除されたときは、ループを止めて待ちます。識別子は解放されているので、人がもう一度認めれば戻れます。デーモンはそのあいだ、空へ確認を打ち続けません。鍵が世代遅れで切られただけなら、手持ちの鍵で間隔を空けて再試行します。この二つを同じ「知らない鍵」に潰すと、削除が、一覧から隠しただけに戻ります。

準備状態は、デーモンが断言する真偽値にしません。阻害を表すコードを制御プレーンが解釈します。生存は、最後の確認からの時間だけで決めます。二分程度より新しければ在席、それより前なら不在、一度も無ければ未着、です。ライフサイクルは、最後に何と言ったかと、その後も打ち続けているかの組です。停止と言ってから沈黙したときだけ、停止とみなします。稼働中と言ったあとに沈黙したときは不明であり、停止ではありません。クラッシュも、スリープも、ネットワークの死も、ここには同じように見えます。未来についての自己申告は保存しません。

表示名は、最初に名前が載った確認で一度だけ採用します。デーモンは定期的に OS のホスト名を送ります。毎回それで上書きすると、人が付けた名前をマシンが取り消します。起動直後で探査が終わっていない欠測は、阻害が無いという意味に読みません。保存してある阻害と準備状態は、そのまま残します。

制御プレーンからマシンへの依頼は、生存確認の応答に載せます。ダイヤルする経路が無いからです。ログの末尾が欲しい、という依頼は、行に時刻として書きます。デーモンは次の応答でその時刻を見ます。アップロードは別の外向き送信で、確認の処理からは外します。確認が転送を待つと、遅いディスクがオフラインに見えます。同じ時刻への再送は一度だけにし、失敗したときだけ次の確認でやり直します。応答に同じ時刻が残っていても、成功済みなら送り直しません。依頼した時刻と、アップロードがエコーした時刻が一致したときだけ、依頼を消します。別の依頼が途中で重なっても、先のアップロードが後の依頼を消しません。マシンが止まっているあいだの依頼は失敗ではありません。次に確認が戻ったときに届きます。

作業の取得も、マシン発の短い要求です。生存確認とは分けました。静かなマシンのコストは、見ているディレクトリの数ではなく、マシンの数に比例させます。一台が複数の場所を見ていても、生存の接続は一つで足ります。宛先がこのインストールに固定された作業は、沈黙しているあいだはここで待ちます。

データ面は、人がいるときだけ SSH

人がいるあいだ、読み書きは人からマシンへ SSH で流れ、変更の通知はマシンから人へ戻ります。人がいないあいだ、データ面の矢印は待ちます。外向きの確認は、それと一緒には待ちません。確認は続きます。

人がそのマシンで対話するときは、既存の SSH の転送で、先の UNIX ソケットに着けます。要求のたびに新しい TCP や暗号の握手を始めるのではなく、張ってある接続の上にチャネルを足します。コストは、呼び出し一回ぶんの往復に近いです。人が操作するペースの読み書きなら、これで足ります。変更の通知は、先のマシンが見て、こちらへ流します。経路の向こうを定期的に全部読み直すことはしません。

転送が届くのは、認証した OS アカウントのソケットまでです。同じ筐体の別アカウントは、別のソケット、別の資格情報、別の行を持ちます。筐体を共有しても、アカウントを横断しません。どの作業場を見せるかは、マシンの資格情報とは別に認可します。マシンは人に属し、ある作業場から見えるのは、そこへ結び付けたディレクトリまでです。他の作業場の一覧には使いません。

SSH の宛先とホスト鍵は、操作する側の手元に置きます。制御プレーンの身元にはしません。人が SSH できないあいだも、生存確認と作業の取得は外向きのまま続きます。データ面だけが、その人の経路が戻るまで待ちます。

何が解けるようになったか

ポートを開けず、常時の VPN も張らずに、NAT の向こうのインストールを一台として数え、生存を見られます。制御プレーンはアドレスを持たないので、NAT の対応表が切れても、次の外向き確認で関係が戻ります。

UI を閉じてもデーモンは残ります。閉じたことは停止ではありません。意図した停止、失効、ただの消失は、同じ「見えない」に潰れません。在席しているが作業を受けられない、という状態も、不在とは別の文で返せます。

一台を削除すると、その資格情報では次から動けません。削除と失効を分けて残すと、次の確認が削除を取り消します。

ログのように、今は入れないが次に起きたら欲しいものは、ダイヤルせずに依頼できます。依頼は確認の応答で届き、答えはマシンからの別の送信で戻ります。

人が接続しているあいだは、データ面をその SSH に載せられます。接続していないあいだも、マシンとしての生存と、固定した作業の待機は残ります。

秘密はプロセス一覧にもサービス定義にも出ません。新しいマシンの承認は、人の操作に閉じます。同じ筐体の別アカウントは、別のマシンとして扱えます。

通信の形を一つにまとめると、届かなさ、セッションの短さ、データ面の大きさ、身元の強さが、同じ経路の上で同時に満たせなくなります。分けたのは、それぞれが失敗する条件が違うからです。