🙆‍♀️

Catalyst 9300のapp hostingでsshサーバーを構築してみる

2024/12/08に公開

MMA Advent Calendar 2024 9日目の記事です

はじめに

Cisco Catalyst 9300では,Dockerアプリケーションホスティングが利用できます.

これを利用することで,L3スイッチなのにLinuxライクに様々なアプリケーションを動かすことができます.

というわけで,今回はsshサーバーを構築してみました.

今回の環境

インターフェース サブネット sshサーバーのIPアドレス ゲートウェイ
グローバル Vlan10 10.0.0.0/24 10.0.0.2 10.0.0.254
ローカル Vlan20 192.168.0.0/24 192.168.0.2 192.168.0.254

グローバルとローカルの,どちらからもアクセスできるようにします.

スイッチの準備

ioxの有効化

まず,ioxが有効になっていなければ,有効化します.

コンフィグレーションに下記を追加してください.

startup-config
iox

AppGigabitEthernetの設定

続いて,app hosting用のインターフェースを設定します.

複数のvlanを扱いたいので,trunkにします.

startup-config
interface AppGigabitEthernet1/0/1
 switchport trunk allowed vlan 10,20
 switchport mode trunk

app hostingの設定

app hostingでは,動かすコンテナごとにapp idを付与する必要があります.

今回はssh_minimalにしました.

startup-config
app-hosting appid ssh_minimal

まず,仮想NICの設定をして,IPアドレスを付与します.

startup-config
app-hosting appid ssh_minimal
 app-vnic AppGigabitEthernet trunk
  vlan 10 guest-interface 1
   guest-ipaddress 10.0.0.2 netmask 255.255.255.0
  vlan 20 guest-interface 0
   guest-ipaddress 192.168.0.2 netmask 255.255.255.0
 app-default-gateway 10.0.0.254 guest-interface 1
 name-server0 8.8.4.4
 name-server1 8.8.8.8

dockerのコマンドオプションを指定します.

startup-config
app-hosting appid ssh_minimal
 app-resource docker
  run-opts 1 --restart=unless-stopped

最後に,使用するリソース単位を設定します.

startup-config
app-hosting appid ssh_minimal
 app-resource profile custom
  cpu-percent 20
  memory 512
  persist-disk 512
end

memorypersist-diskの単位はMBです.

以上のコンフィグを適用して保存したら,準備完了です.

Docker コンテナのビルド

docker composeは使えません.Dockerのtarボールを直接スイッチに移す形になります.

そのため,やりたいことはすべてDockerfileに書きます.

Dockerfile

app hostingではchrootができません.

そのため,sshd_configUsePrivilegeSeparationnoにしておく必要があります.

ただし,noにした場合,sshdがrootで動いてしまい,セキュリティ的に大変脆弱です.

そのため,sshdを非特権ユーザーで動かすようにすることで,リスクの軽減を図ります.
したがって,今回は単一ユーザーだけがログインできるsshサーバーとなります.

非特権ユーザーで動かすためのsshd_configは以下のようになります.
mmaを非特権ユーザーにした場合です.

sshd_config
Port 2222

HostKey /home/mma/.sshd/ssh_host_rsa_key
HostKey /home/mma/.sshd/ssh_host_ecdsa_key
HostKey /home/mma/.sshd/ssh_host_ed25519_key
PidFile /home/mma/.sshd/sshd.pid

PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
UsePAM yes

AllowTcpForwarding yes
X11Forwarding no

PrintMotd no
AcceptEnv LANG LC_*

UsePrivilegeSeparation no

特権ポートが使えなくなるため,ポート番号は2222に変えました.

また,パスワード認証も無効化しています.

Dockerfileは,ベースをUbuntu 24.04にした場合は以下の通りとなります.

Dockerfile
FROM ubuntu:24.04

RUN apt-get update && apt-get install -y \
    openssh-server openssl sudo iputils-ping curl vim nano busybox
RUN busybox --install -s

COPY sshd_config /etc/ssh/sshd_config
RUN mkdir -p /run/sshd

ARG SSH_USER
RUN useradd -m ${SSH_USER} -s /bin/bash -G sudo
RUN --mount=type=secret,id=ssh_password \
    echo "${SSH_USER}:$(openssl passwd -6 $(cat /run/secrets/ssh_password))" | chpasswd -e

USER ${SSH_USER}
RUN mkdir -p /home/${SSH_USER}/.sshd
RUN ssh-keygen -f /home/${SSH_USER}/.sshd/ssh_host_rsa_key -N '' -t rsa
RUN ssh-keygen -f /home/${SSH_USER}/.sshd/ssh_host_ecdsa_key -N '' -t ecdsa
RUN ssh-keygen -f /home/${SSH_USER}/.sshd/ssh_host_ed25519_key -N '' -t ed25519

RUN mkdir -p /home/${SSH_USER}/.ssh
COPY (ssh公開鍵のパス) /home/${SSH_USER}/.ssh/authorized_keys
RUN chmod -R 600 /home/${SSH_USER}/.ssh

ENTRYPOINT ["/usr/sbin/sshd", "-D"]

apt-getで使いたいものをあらかじめインストールしています.unminimizeをしてもいいとは思いますが,今回はしませんでした.

また,sudoコマンドを使うときのために,パスワードも書き込んでいます.

さらに,公開鍵認証をする必要があるため,そのクライアント証明書もコピーしています.

パスワードの設定の仕方は後述します.

ビルドコマンド

Dockerfileとsshd_config,パスワードが書かれたテキストファイルが用意できたら,
以下のコマンドでビルドできます.

$ docker build --secret id=ssh_password,src=${SSH_PASSWORD_FILE} --build-arg SSH_USER=${SSH_USER} -t ssh-minimal .

tagはssh-minimalとしました.

変数SSH_USERは,非特権ユーザーのユーザー名です.例えば,sshd_configが先述と同一の場合,mmaとなります.

変数SSH_PASSWORD_FILEは,非特権ユーザーのパスワードが書かれたファイルのパスです.

tarボールの保存

ビルドしても,カレントディレクトリにイメージが保存されるわけではありません.

イメージを保存するために,下記のコマンドを実行します.

$ docker save -o ssh-minimal.tar ssh-minimal
$ ls -l ssh-minimal.tar
-rw------- 1 shiragi shiragi 317933568 Nov 19 09:19 ssh-minimal.tar

Makefile

ビルドとtarボールの保存をMakefileにすると以下のようになります.

Makefile
include .env

ssh-minimal.tar: build
	docker save -o ssh-minimal.tar ssh-minimal

.PHONY: build clean
build: Dockerfile sshd_config ${SSH_PASSWORD_FILE}
	docker build  --secret id=ssh_password,src=${SSH_PASSWORD_FILE} --build-arg SSH_USER=${SSH_USER} -t ssh-minimal .

clean:
	docker rmi ssh-minimal
	rm -f ssh-minimal.tar

Dockerアプリケーションの起動

tarボールをコピー

tarボールをスイッチのフラッシュメモリにコピーします.

scpやUSBメモリで移したりといろいろ方法はありますが,

例えばスイッチ上のコマンドラインから,sftpでコピーする場合は以下の感じになります.

Switch# copy sftp://(クライアントのユーザー)@(クライアントのIP)/(tarファイルのフルパス) flash:/ssh-minimal.tar

クライアントからscpで送信する場合は以下のコマンドです.

$ scp ssh-minimal.tar (スイッチの特権ユーザー)@(スイッチのIP):flash:/ssh-minimal.tar

しかし,私の環境のうち,2台中1台では,scpを有効にしているにもかかわらずConnection closedと返されてコピーできない筐体がありました.ただし,sftpではできました.

tarボールからDockerイメージをインストール

スイッチ上で下記のコマンドを実行することで,コピーしたtarボールをインストールできます.

appidには,先述のコンフィグで指定したappidと一致させます.

Switch# app-hosting install appid ssh_minimal package flash:/ssh-minimal.tar

Dockerアプリの有効化

下記のコマンドでインストールしたDockerアプリを有効化できます.

このとき,コンフィグに問題があるとエラーが出ます.

Switch# app-hosting activate appid ssh_minimal

Dockerアプリの起動

無事,有効化できたら,下記のコマンドでスタートさせます.

Switch# app-hosting start appid ssh_minimal

sshの接続

起動できたら,ssh接続してみましょう.

$ ssh -p 2222 192.168.0.2
To run a command as administrator (user "root"), use "sudo <command>".
See "man sudo_root" for details.

mma@26df14e31b73:~$

無事,接続できました.

試しにip aコマンドを実行してみます.先述のDockerfileでbusyboxをインストールしているため,Ubuntu minimizedですがipコマンドが使えます.

mma@26df14e31b73:~$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: tunl0@NONE: <NOARP> mtu 1480 qdisc noop qlen 1000
    link/ipip 0.0.0.0 brd 0.0.0.0
3: ip_vti0@NONE: <NOARP> mtu 1480 qdisc noop qlen 1000
    link/ipip 0.0.0.0 brd 0.0.0.0
61: sss@if62: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue
    link/ether 52:54:99:99:00:00 brd ff:ff:ff:ff:ff:ff
    inet 192.168.10.66/27 brd 192.168.10.95 scope global sss
       valid_lft forever preferred_lft forever
    inet6 fe80::5054:99ff:fe99:0/64 scope link
       valid_lft forever preferred_lft forever
63: eth0@if64: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue qlen 1000
    link/ether 52:54:dd:8a:0c:bb brd ff:ff:ff:ff:ff:ff
    inet 192.168.0.2/24 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::5054:ddff:fe8a:cbb/64 scope link
       valid_lft forever preferred_lft forever
65: eth1@if66: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue qlen 1000
    link/ether xx:xx:xx:xx:xx:xx brd ff:ff:ff:ff:ff:ff
    inet 10.0.0.2/24 scope global eth1
       valid_lft forever preferred_lft forever
    inet6 fe80::5054:ddff:fe83:992f/64 scope link
       valid_lft forever preferred_lft forever
mma@26df14e31b73:~$

コンフィグ通りにIPアドレスが付与されていることが確認できました.

Dockerアプリのアンインストール

スイッチからDockerアプリをアンインストールしたい場合は,
上記とは反対のコマンドを実行します.

Switch# app-hosting stop appid ssh_minimal
Switch# app-hosting deactivate appid ssh_minimal
Switch# app-hosting uninstall appid ssh_minimal

おわりに

特権ポートが使えなかったり,単一ユーザーしかログインできなかったりと,いろいろ制約はありましたが,何とか構築できました.

スイッチにsshサーバーがあることで,ポートフォワーディングを利用してローカル内にあるサーバーのIPMIをアクセスして,遠隔でサーバーの電源を点けたりと様々なことに利用できそうです.

また,sshサーバーだけでなく,今アツイtailscaleを動かすこともできそうです.

参考

https://www.cisco.com/c/ja_jp/td/docs/ios-xml/ios/prog/configuration/172/b_172_programmability_cg/application_hosting.html

Discussion