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