Linuxのsshdをネットワーク的に隔離したい話

GMOインターネットの梅崎です。
今回はLinuxのnetwork namespace(以降、netns)を利用し、SSHなどの管理系のサービスをネットワーク的に隔離した話です。

何をしたいのか?

管理系通信(SSH等)とサービス通信(HTTP等)で別のルーティングテーブルを参照させたい。

特にKubernetesにてL3でのPod間通信をするCNIを採用した場合など
サーバー間通信を「L2内+デフォルトルート」→「動的ルーティング」にした際に
「サービス通信の動的ルーティング」と「管理系通信の静的ルーティング」を分離し、
管理をシンプルにしたいという意図がありました。

先に結論

・管理系通信用のnetnsを作成するservice
・インターフェースのnetnsへの移動およびIP等を設定するservice
・netnsでiptablesの設定をするservice
・各serviceをnetns側で起動させるためのdrop-in

等を作成し、ひとまず問題なく動作しています。
この後に具体的な設定例などを記載しています。

なぜこのやり方?

netns vs VRF

VRF機能は期待した挙動ではなかったため今回は見送り。

なぜネットワーク設定を自作のserviceとスクリプトで?

Netplan等で設定をしたかったが、netnsをサポートしていない。
https://netplan.readthedocs.io/en/latest/netplan-yaml/

systemd-networkdは別のnetnsのインターフェース等の設定変更をサポートしていない。(↑の背景でもある)
systemd-networkd自体を複数立ち上げる方法もあるようだが
やり方が怪しそうなため見送り。

https://github.com/systemd/systemd/issues/11103
(↑で実装するのか?のような議論自体はされていました)

詳細手順

設定ミスなどがあるとリモートから操作できなくなる可能性が高いため、
コンソール等からのログインをおすすめします。

※一部のスクリプトはチャッピーくんの出力を手直し・改造したものです。
また、サービス提供を前提とした手順にはなっていない点、あらかじめご了承ください。
(一応、手元では動作しています)

前提

・管理系通信用のnetnsの名前は「mgmt-ns」
・OSは「Ubuntu 24.04.3 LTS (6.8.0-79-generic)」
・systemdは「systemd 255 (255.4-1ubuntu8.10)」

新規netnsが初期netnsのカーネルパラメーターを継承するようにする

# 新規netnsのカーネルパラメーターについて
# devconf_inherit_init_netが初期値だと「0(IPv4は継承/IPv6は初期化)」のため
# 「1(IPv4/IPv6ともに継承)」に変更する
# (「/proc/sys/net/ipv{4,6}/conf/{all,default}/」が対象、ほかのパラメーターは別途対応してください)
echo 'net.core.devconf_inherit_init_net = 1' >> /etc/sysctl.d/99-netns.conf

おすすめの設定

# 初期のnetnsは動的ルーティング前提、mgmt-nsはこの後に別途設定されるためか
# オンラインかのチェックに失敗しタイムアウトを待ってしまうので無効化しておく
systemctl disable --now systemd-networkd-wait-online.service
systemctl mask systemd-networkd-wait-online.service

netnsを作るservice

mkdir -p '/etc/netns/mgmt-ns/'

# netns内で使いたいresolverを指定
cat << 'EOS' > '/etc/netns/mgmt-ns/resolv.conf'
nameserver 1.1.1.1
nameserver 8.8.8.8

EOS

cat << 'EOS' > '/etc/systemd/system/[email protected]'
[Unit]
Description=Create network namespace %i
DefaultDependencies=no
After=network-pre.target
Wants=network-pre.target

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/bin/ip netns add %i
ExecStart=/usr/bin/ip -n %i link set lo up

[Install]
WantedBy=multi-user.target

EOS

netnsにインターフェースを移動し、移動したインターフェースの設定をするservice

cat << 'EOS' > '/etc/systemd/system/[email protected]'
[Unit]
Description=Configure IP/route for %i in mgmt-ns
After=sys-subsystem-net-devices-%i.device
[email protected]
[email protected]

[Service]
Type=simple
ExecStart=/usr/local/libexec/mgmt-ns-netwatch.sh mgmt-ns %i
Restart=always
RestartSec=2s

[Install]
WantedBy=sys-subsystem-net-devices-%i.device

EOS
mkdir -p '/usr/local/libexec/'

cat << 'EOS' > '/usr/local/libexec/mgmt-ns-ifconfig.sh'
#!/usr/bin/env bash
set -euo pipefail
set -f  # pathname 展開を抑止

NS="${1:?NS required}"   # 例: mgmt-ns
IF="${2:?IF required}"   # 例: enp3s0

source "/etc/mgmt-ns/${IF}.env"

# EnvironmentFile から渡される変数(未設定は空)
ADDR4="${ADDR4:-}"      # セミコロン区切りで複数可: "10.100.0.10/24; 10.100.0.11/24"
GW4="${GW4:-}"          # 例: "10.100.0.1"
ROUTES4="${ROUTES4:-}"  # セミコロン区切り: "192.0.2.0/24 via 10.100.0.254; 198.51.100.0/24 via 10.100.0.253"

ADDR6="${ADDR6:-}"
GW6="${GW6:-}"
ROUTES6="${ROUTES6:-}"

MTU="${MTU:-}"          # 例: "9000"

trim() {
  # 先頭/末尾の空白を除去
  local s="${1}"
  s="${s#"${s%%[![:space:]]*}"}"
  s="${s%"${s##*[![:space:]]}"}"
  printf '%s' "${s}"
}

# 1) IF の所在判定
in_host=0 in_ns=0
if [[ -e "/sys/class/net/${IF}" ]]; then
  in_host=1
fi
if ip -n "${NS}" link show dev "${IF}" &>/dev/null; then
  in_ns=1
fi

if (( in_host == 0 && in_ns == 0 )); then
  echo "interface ${IF} not found in host nor in ${NS}" >&2
  exit 1
fi

# 2) 必要なら host→netns へ移動
if (( in_host == 1 && in_ns == 0 )); then
  ip link set dev "${IF}" netns "${NS}"
  in_ns=1
fi

# リンク UP / MTU
ip -n "${NS}" link set dev "${IF}" up
if [[ -n "${MTU}" ]]; then
  ip -n "${NS}" link set dev "${IF}" mtu "${MTU}"
fi

# 既存アドレスを整理
ip -n "${NS}" -4 addr flush dev "${IF}"
ip -n "${NS}" -6 addr flush dev "${IF}"

# IPv4 アドレス(セミコロン区切りを配列化して安全に追加)
if [[ -n "${ADDR4}" ]]; then
  IFS=';' read -r -a _addr4_arr <<< "${ADDR4}"
  for a in "${_addr4_arr[@]}"; do
    a="$(trim "${a}")"
    [[ -z "${a}" ]] && continue
    ip -n "${NS}" -4 addr add "${a}" dev "${IF}"
  done
fi

# IPv6 アドレス
if [[ -n "${ADDR6}" ]]; then
  IFS=';' read -r -a _addr6_arr <<< "${ADDR6}"
  for a in "${_addr6_arr[@]}"; do
    a="$(trim "${a}")"
    [[ -z "${a}" ]] && continue
    ip -n "${NS}" -6 addr add "${a}" dev "${IF}"
  done
fi

# デフォルトルート
if [[ -n "${GW4}" ]]; then
  ip -n "${NS}" -4 route add default via "${GW4}" dev "${IF}"
fi
if [[ -n "${GW6}" ]]; then
  ip -n "${NS}" -6 route add default via "${GW6}" dev "${IF}"
fi

# 追加ルート(セミコロン区切りを配列化 → 要素ごとに単語分割して渡す)
if [[ -n "${ROUTES4}" ]]; then
  IFS=';' read -r -a _r4_arr <<< "${ROUTES4}"
  for r in "${_r4_arr[@]}"; do
    r="$(trim "${r}")"
    [[ -z "${r}" ]] && continue
    read -r -a args <<< "${r}"
    ip -n "${NS}" -4 route add "${args[@]}"
  done
fi

if [[ -n "${ROUTES6}" ]]; then
  IFS=';' read -r -a _r6_arr <<< "${ROUTES6}"
  for r in "${_r6_arr[@]}"; do
    r="$(trim "${r}")"
    [[ -z "${r}" ]] && continue
    read -r -a args <<< "${r}"
    ip -n "${NS}" -6 route add "${args[@]}"
  done
fi

EOS

cat << 'EOS' > '/usr/local/libexec/mgmt-ns-netwatch.sh'
#!/usr/bin/env bash
set -euo pipefail

NS="${1:?NS required}"   # 例: mgmt-ns
IF="${2:?IF required}"   # 例: enp3s0

/usr/local/libexec/mgmt-ns-ifconfig.sh "${NS}" "${IF}"

# netns 内のリンクイベントを監視
# その IF に関する "state UP" / "LOWER_UP" / 再出現 を検知したら再適用
ip -n "${NS}" monitor link | while read -r line; do
  # 自分の IF 以外はスキップ
  [[ "${line}" == *": ${IF}:"* ]] || continue

  case "${line}" in
    *"state UP"*|*"LOWER_UP"*)
      /usr/local/libexec/mgmt-ns-ifconfig.sh "${NS}" "${IF}"
      ;;
    *"Deleted"*)
      # デバイスが消えた(ホットプラグ/VM再生成等)。戻るまで待って再適用
      while ! ip -n "${NS}" link show dev "${IF}" &>/dev/null; do sleep 1; done
      /usr/local/libexec/mgmt-ns-ifconfig.sh "${NS}" "${IF}"
      ;;
  esac
done

EOS

chmod 700 '/usr/local/libexec/mgmt-ns-ifconfig.sh'
chmod 700 '/usr/local/libexec/mgmt-ns-netwatch.sh'
# netnsへ移動したいインターフェースの名前
_IF_NAME=''

mkdir -p '/etc/mgmt-ns/'

cat <<'EOS' > "/etc/mgmt-ns/${_IF_NAME}.env"
ADDR4='192.0.2.10/24'
GW4='192.0.2.1'
# 必要に応じて:
# ROUTES4='198.51.100.0/24 via 192.0.2.1; 203.0.113.0/24 via 192.0.2.1'
# ADDR6='2001:db8:10::10/64'
# GW6='2001:db8:10::1'
# ROUTES6=''
# MTU='9000'

EOS

netns内でiptablesを設定するservice

# '/etc/iptables'も存在しないとエラーになる
mkdir -p '/etc/iptables'
mkdir -p '/etc/netns/mgmt-ns/iptables'

# iptables-restoreで読める形式で記載
#/etc/netns/mgmt-ns/iptables/rules.v4
#/etc/netns/mgmt-ns/iptables/rules.v6
cat <<'EOS' > '/etc/systemd/system/[email protected]'
[Unit]
Description=iptables-restore in namespace %i
After=netns@%i.service
Wants=netns@%i.service

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/libexec/mgmt-ns-iptables-restore.sh %i
ExecReload=/usr/local/libexec/mgmt-ns-iptables-restore.sh %i
Restart=on-failure

[Install]
WantedBy=multi-user.target

EOS
mkdir -p '/usr/local/libexec/'

cat <<'EOS' > '/usr/local/libexec/mgmt-ns-iptables-restore.sh'
#!/usr/bin/env bash
set -euo pipefail

NS="${1:?NS required}"   # 例: mgmt-ns

DIR="/etc/netns/${NS}/iptables"
RULES_V4_PATH="${DIR}/rules.v4"
RULES_V6_PATH="${DIR}/rules.v6"

# v4/v6 それぞれ存在したら適用(ロック待ち付き)
if [ -f "${RULES_V4_PATH}" ]; then
  ip netns exec "${NS}" iptables-restore  -w 5 "${RULES_V4_PATH}"
fi

if [ -f "${RULES_V6_PATH}" ]; then
  ip netns exec "${NS}" ip6tables-restore -w 5 "${RULES_V6_PATH}"
fi

EOS

chmod 700 '/usr/local/libexec/mgmt-ns-iptables-restore.sh'

設定の反映

※sshで入って作業をしている場合はここを一旦飛ばし
「netns内で各サービスが起動するようにする」のsshdの設定等の後に
下記の「SSHで入っている場合、自動起動有効化」のみを
実施しOSごと再起動することを推奨します。
(その場合は systemctl restart はしないこと)

#SSHで入っていない場合、自動起動有効化&起動
systemctl daemon-reload

systemctl enable --now [email protected]
systemctl enable --now mgmt-ns-netconfig@${_IF_NAME}.service
systemctl enable --now [email protected]
#SSHで入っている場合、自動起動有効化のみ
systemctl daemon-reload

systemctl enable [email protected]
systemctl enable mgmt-ns-netconfig@${_IF_NAME}.service
systemctl enable [email protected]

netns内で各サービスが起動するようにする

# 例
mkdir -p '/etc/systemd/system/HOGEHOGE.service.d/'

cat <<'EOS' > '/etc/systemd/system/HOGEHOGE.service.d/mgmt-ns.conf'
[Unit]
[email protected]
[email protected]

[Service]
NetworkNamespacePath=/var/run/netns/mgmt-ns
BindReadOnlyPaths=/etc/netns/mgmt-ns/resolv.conf:/etc/resolv.conf

EOS

systemctl daemon-reload

# systemdのsocketはデフォルトのnetnsになるため、サービスによってはsocketを無効化する必要がある
systemctl disable --now HOGEHOGE.socket
systemctl mask HOGEHOGE.socket

# いずれか
systemctl enable --now HOGEHOGE.service
systemctl restart HOGEHOGE.service
# sshd
mkdir -p '/etc/systemd/system/ssh.service.d/'

cat <<'EOS' > '/etc/systemd/system/ssh.service.d/mgmt-ns.conf'
[Unit]
[email protected]
[email protected]

[Service]
NetworkNamespacePath=/var/run/netns/mgmt-ns
BindReadOnlyPaths=/etc/netns/mgmt-ns/resolv.conf:/etc/resolv.conf

EOS

systemctl daemon-reload
systemctl disable --now ssh.socket
systemctl mask ssh.socket

systemctl restart ssh.service
# このmgmt-ns内のsshdからログインした場合に、デフォルトのnetnsへ移る方法
# sudo nsenter --net=/proc/1/ns/net --mount=/proc/1/ns/mnt -- /bin/bash -l


# systemd-timesyncd
mkdir -p '/etc/systemd/system/systemd-timesyncd.service.d/'

cat <<'EOS' > '/etc/systemd/system/systemd-timesyncd.service.d/mgmt-ns.conf'
[Unit]
[email protected]
[email protected]

[Service]
NetworkNamespacePath=/var/run/netns/mgmt-ns
BindReadOnlyPaths=/etc/netns/mgmt-ns/resolv.conf:/etc/resolv.conf

EOS

systemctl daemon-reload

systemctl restart systemd-timesyncd

ブログの著者欄

梅崎 皓太

GMOインターネットグループ株式会社

ネットワークエンジニア。GMOインターネットグループ株式会社のバックボーンから商材までのネットワーク設計・構築・運用を担当しています。 写真はマイニング事業で北欧に飛んでいたときにブリザードに遭遇した後の写真です。冷たすぎてiPhone電源落ちちゃってビックリ

採用情報

関連記事

KEYWORD

TAG

もっとタグを見る

採用情報

SNS FOLLOW

GMOインターネットグループのSNSをフォローして最新情報をチェック