About Portfolio Posts Tags

Incus上のDebian 13コンテナにSSHサーバーを設定してログインできるようにする方法

最近開発環境をVPSに移行しようと思いIncus上のDebian13コンテナにSSHサーバを設定したのですが、色々引っかかった点があったため記録しておきます。

本記事では、作成するコンテナの名前をmycontainer とします。

前提

  • Incusがホストにインストール済みであること
  • 対象コンテナ:Debian 13(trixie)
  • ゴール:鍵認証のみでSSHログインできる状態にする(パスワード認証は無効化)

結論

コンテナ作成時にcloud-initで全部設定してしまうのが一番事故が少ないです。

作成後に手作業でapt installsshd_config編集をやると、以下のような問題に高確率でぶつかりますし、この記事を読んでいるということはIncusコンテナにSSHサーバを設定したことがない or 不慣れと思われるためヒューマンエラーの原因になります。

罠1: images:debian/13にはcloud-initが入っていない

Incusのイメージには通常版と/cloudサフィックス付きの版があります。

# NG: cloud-initが存在せず、user-dataを渡しても無視される
incus launch images:debian/13 mycontainer --config=cloud-init.user-data="..."

# OK
incus launch images:debian/13/cloud mycontainer --config=cloud-init.user-data="..."

通常版(minimalバリアント)はサイズを抑えるためcloud-init自体が同梱されていません。--configでuser-dataを渡しても何も起きず、素のコンテナがそのまま立ち上がるだけなので気づきにくいです。

cloud-init status --waitを作成後に必ず実行して確認しましょう。

罠2: lock_passwd: trueだけだとsudoがデッドロックする

パスワード認証を無効化したい場合、cloud-initのusers設定でlock_passwd: trueにしてパスワードを未設定にしたくなります。

しかしこれだけだと、Debianのデフォルトsudoers設定では「sudo実行時にパスワードを要求されるが、そのユーザーには入力できるパスワードが存在しない」というデッドロックになります。

鍵認証でSSHログインはできても、sudoが一切通らないアカウントが出来上がってしまいます。

users:
  - name: myuser
    groups: sudo
    shell: /bin/bash
    sudo: ALL=(ALL) NOPASSWD:ALL   # これが無いとsudoが詰む
    lock_passwd: true
    ssh_authorized_keys:
      - ssh-ed25519 AAAA...

罠3: systemdユニット名はsshdではなくssh

RHEL系の知識でsystemctl status sshdと打つと、Debian/Ubuntu系では 「Unit sshd.service could not be found」と言われます。正しくはsshです。

systemctl status ssh
systemctl restart ssh

紛らわしいですがssh_config(クライアント設定)とsshd_config(サーバー設定)は

別ファイルで役割も逆です。sshd側の挙動を変えたいなら編集すべきはsshd_configの方です。

cloudinitを使わず設定しようとしてたときにssh_configがあって後者がなかったため間違えて編集してしまいました、、

cloud-init設定ファイル(完成版)

上記の罠を踏まえた、実際に動作確認済みの設定です。journal永続化とfail2banの 導入も作成時点で済ませてしまいます。

#cloud-config
package_update: true
packages:
  - openssh-server
  - fail2ban

users:
  - name: (ここに設定したいユーザー名を入力)
    groups: sudo
    shell: /bin/bash
    sudo: ALL=(ALL) NOPASSWD:ALL
    lock_passwd: true
    ssh_authorized_keys:
      - ssh-ed25519 AAAA...(自分の公開鍵)

write_files:
  - path: /etc/ssh/sshd_config.d/99-hardening.conf
    content: |
      PasswordAuthentication no
      PermitRootLogin no
      PermitEmptyPasswords no
      PubkeyAuthentication yes

  - path: /etc/systemd/journald.conf.d/99-persistent.conf
    content: |
      [Journal]
      Storage=persistent

  - path: /etc/fail2ban/jail.local
    content: |
      [sshd]
      enabled = true
      port = ssh
      backend = systemd
      maxretry = 5
      bantime = 3600
      findtime = 600

runcmd:
  - mkdir -p /var/log/journal
  - systemctl restart systemd-journald
  - systemctl enable --now ssh
  - sshd -t
  - systemctl restart ssh
  - systemctl enable --now fail2ban

journal永続化を先に済ませておくのは、外部公開後に不審なアクセスがないか調査したくなった時のためです。

サービス起動前の期間はどうやっても遡れないので、公開する前に記録が残る状態を作っておく必要があります。

コンテナ作成

incus launch images:debian/13/cloud mycontainer \
  --config=cloud-init.user-data="$(<cloud-init.yaml)"

作成後の確認

cloud-initが実際に走ってすべて反映されているか、まとめて確認します。

sudo incus exec mycontainer -- cloud-init status --wait
sudo incus exec mycontainer -- systemctl status ssh
sudo incus exec mycontainer -- sshd -t
sudo incus exec mycontainer -- grep -i '^Include' /etc/ssh/sshd_config
sudo incus exec mycontainer -- systemctl status fail2ban
sudo incus exec mycontainer -- fail2ban-client status sshd
sudo incus exec mycontainer -- ls -la /var/log/journal

外部からアクセスできるようにする(proxy device)

Incusのproxy deviceでホストのポートをコンテナの22番に転送します。

sudo incus config device add mycontainer ssh-proxy proxy \
  listen=tcp:0.0.0.0:2222 \
  connect=tcp:127.0.0.1:22

罠4: nat=trueはワイルドカードlistenと併用できない

kernel-mode NAT(nat=true)の方が性能は良いのですが、以下のようにlistenをワイルドカードにすると弾かれます。

Error: Invalid devices: Device validation failed for "ssh-proxy": Cannot listen on
wildcard address "0.0.0.0" when in nat mode

nat=trueを使うにはコンテナに静的IPを設定する必要があり、DHCP運用だと面倒が増えます。

上記の設定の方がシンプルで便利なため私はこちらを使いました。

接続確認

# 鍵認証でログインできること
ssh -p 2222 設定したユーザー名@<ホストのグローバルIP>

# パスワード認証は拒否されること
ssh -p 2222 -o PubkeyAuthentication=no 設定したユーザー名@<ホストのグローバルIP>

後者はPermission denied (publickey)で拒否されれば正しい設定です。

まとめ

以上私がやった設定方法でした。

cloudinitを使うのは初めてでしたが便利だと思いました。

参考になれば、、!