ECS Fargate を Tailscale につないでスマホから Termius で SSH 接続する

awslinux

出先からも Taskwarrior を使うため ECS Fargate のコンテナに SSH 接続したい。

Taskwarrior と Timewarrior でコマンドラインからタスク管理とタイムトラッキングをする - sambaiz-net

iPhone (Termius)
    ↓ SSH
Fargate Task
└── Container (Debian)
    ├── tailscaled (userspace networking + SSH server)
    └── EFS mount: /var/lib/tailscale

セキュアに繋ぐにあたって AWS であればポートを公開する必要がない SSM が選択肢に上がるが、スマホのような AWS CLI が動かせない環境だと使えないので Tailscale を使う。Tailscale は Coordination Server が接続情報を返した後は Linux カーネルにも組み込まれている VPN プロトコル WireGuard でエンドツーエンド暗号化して P2P 通信を行うサービス。パケットを送り合う UDP hole punching で NAT トラバーサルするので SSM と同じくインバウンドのポートを開く必要がない。

ssh-over-ssm で Session Manager を通して EC2 インスタンスに SSH 接続する - sambaiz-net

Tailscale をインストールする。

RUN curl -fsSL https://tailscale.com/install.sh | sh

Tailscale は通常 TUN (network TUNnel) デバイスでルーティングおよびハンドリングを行う。

$ ip tuntap add dev tun0 mode tun
$ ip addr add 10.200.0.1/24 dev tun0
$ ip link set tun0 up

$ ip route | grep tun0
# 10.200.0.0/24 dev tun0 proto kernel scope link src 10.200.0.1 linkdown

$ ip tuntap del dev tun0 mode tun

ただ Fargate のような TUN をサポートしていない環境ではこの方法が取れないので tailscaled を SOCKS5/HTTP プロキシとする userspace networking mode で動かす。

$ tailscaled --state=/var/lib/tailscale/tailscaled.state --tun=userspace-networking &

再起動後も同じデバイスとして認識させるため /var/lib/tailscale を EFS に永続化させる。

const fileSystem = new efs.FileSystem(this, 'Efs', { vpc });

const tailscaleAccessPoint = fileSystem.addAccessPoint('TailscaleAccessPoint', {
  path: '/tailscale',
  posixUser: { uid: '0', gid: '0' },
});

taskDef.addVolume({
  name: 'tailscale-state',
  efsVolumeConfiguration: {
    fileSystemId: fileSystem.fileSystemId,
    authorizationConfig: { accessPointId: tailscaleAccessPoint.accessPointId },
  },
});

container.addMountPoints({
  sourceVolume: 'tailscale-state',
  containerPath: '/var/lib/tailscale',
  readOnly: false,
});

authkey をコンソールで生成し tailscale up を実行すると keypair が生成され、公開鍵は Coordination Server に登録されて WireGuard での暗号化に使われる。キーはデフォルトで 180 日の有効期限があるので、サーバー用途では Disable key expiry を設定する。

$ tailscale up --authkey="${TS_AUTHKEY}" --ssh --reset

–ssh は Tailscale SSH を有効にするフラグ。tailscaled がポート 22 をインターセプトし認証を行う。ACL の設定が必要。

{
  "ssh": [
    {
      "action": "accept",
      "src": ["autogroup:member"],
      "dst": ["autogroup:self"],
      "users": ["root"]
    }
  ]
}

tailscale/gitops-acl-action を使うと push で ACL を反映できる。

on:
  push:
    paths:
      - 'tailscale/policy.hujson'

jobs:
  acl:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: tailscale/gitops-acl-action@v1
        with:
          oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }}
          oauth-secret: ${{ secrets.TS_OAUTH_SECRET }}
          tailnet: ${{ secrets.TS_TAILNET }}
          policy-file: tailscale/policy.hujson
          action: apply

スマホに Tailscale アプリを入れてネットワークに接続すると Termius からデバイス名で SSH 接続できる。