演習進捗0 / 5未完了
⏱ 約 110 分
Part 4Chapter 05/サーバ構築

TLS とファイアウォールで、公開範囲を絞る

演習用証明書の信頼を確かめ、5 台の到達表を変更前後で比べて、必要なサービス経路だけを残します。

教材 ✓ 公開中登録不要 実機演習 ― 提供していない読んで学べます
所要時間約 110 分(概念 20 分+演習 90 分)
前提知識Part 2 の TCP・port・経路・通信制御、Part 3 の SSH・権限、Part 4 Chapter 1〜4 の概念
使用機材入口サーバ front01、アプリサーバ app01・app02、DB サーバ db01、操作端末 workstation01、LAN 内の教材取得経路
関連章前: Part 4 / Chapter 4「2 台へ振り分け、故障した 1 台を経路から外す」/後: Part 4 / Chapter 6「改札サービスを建て直し、運用へ引き渡す」

動く経路を、公開してよい経路へ変える

入口が 2 台の App(タップを処理するアプリケーション)へ要求を振り分けられても、通信相手を確かめる仕組みと、各層へ到達できる送信元の制限がなければ、サービスを引き渡せる状態とは言えません。本章では入口に演習用の TLS(通信を暗号化し、相手を証明書で確かめる仕組み)を設定し、5 台をまたぐ到達性を変更前と変更後で比較します。

本章も初期状態から独立して成立します。Chapter 4 と同じ手順で 4 層を短縮構築しますが、ほかの章の構築物は前提にしません。振り分けと共有状態の理由を確認したい場合は Chapter 4 を参照してください。

期待される出力
                       許可するサービス経路
workstation01 ── HTTPS ──> front01
                              ├──> app01 ──┐
                              └──> app02 ──┴──> db01

変更前: 5 台から各公開面を同じ方法で測る
変更後: 同じ測定を繰り返し、許可・拒否の差だけを見る

この章で受講者が変更するのは、サービスを構成する VM 同士の経路だけです。管理経路や、受講者が触れない層の保護はこの演習では変更しません。また、TLS を 1 台へ設定するだけで終わらず、入口、2 台の App、DB、操作端末を横断して最小経路を証明します。

この章のゴール: 演習用証明書の名前・発行者・期限を読み、信頼前後の違いを説明できる。さらに 5 台から採った到達性の表を変更前後で比較し、入口から DB までのサービス経路だけが許可されたことを証明できる。

この章は、街の「サービスエリア」を作る Part 4 の一部です。街の全体像は Part 0(この教材で作る街)の地図で確かめられます。

扱わない範囲

本章では、次の範囲は扱いません。

この章を終えるとできること

TLS の信頼と層間の到達性を別々に観測し、サービスの公開範囲を根拠付きで絞れるようになります。本章で身につく 4 動作 は次のとおりです。

01
① 証明書を作って入口へ置く
演習用の CA(認証局)と server certificate(サーバ証明書)を作り、SAN(証明書に書く「接続に使える名前」の欄)・期限・発行者を読んでから nginx へ配置する。
02
② 信頼前後を比較する
curl と openssl s_client を使い、暗号化された通信と相手の検証を分けて観測する。
03
③ 公開面を表にする
5 台から入口・App・DB の port へ同じ順序で接続し、変更前の到達性を保存する。
04
④ サービス経路を最小化する
1 ルールずつ適用して管理接続を確認し、変更後の表との差から許可・拒否を説明する。

暗号化、相手の確認、公開範囲を分けて考える

1. 証明書は名前・発行者・期限の 3 点で読む

server certificate は、単に「暗号化を有効にするファイル」ではありません。クライアントは、接続に使った名前が SAN に含まれるか、信頼する発行者へ証明書チェーン(chain:証明書から信頼する CA までのつながり)をたどれるか、現在時刻が有効期間内かを確認します。

期待される出力
名前    : 接続先として指定した名前が SAN にあるか
発行者  : 信頼する CA へ chain をたどれるか
期限    : notBefore から notAfter の範囲内か

本章では openssl x509 で作成物を読み、openssl s_client でサーバが実際に提示した chain を読みます。「作成したファイル」と「通信時に提示されたもの」を同じだと思い込まず、別々に確認します。

2. 暗号化されていることと、相手が正しいことは別

TLS ハンドシェイク(handshake:通信を始める前の確認と取り決め)が始まり暗号化方式を合意できても、クライアントが証明書の発行者を信頼していなければ検証は失敗します。また、発行者を信頼しても、接続名が SAN と一致しなければ別の相手かもしれません。

そのため STEP 5 では、まず CA を指定せずに失敗を観測し、その後で演習用 CA を明示して成功を確認します。失敗を飛ばして成功だけを見ると、何を信頼した結果なのかを説明できません。

3. 公開面は「どこで待つか」と「どこから届くか」の組み合わせ

ss -lntp は各 VM がどの port で待っているかを示します。しかし、待受があるだけでは、どの送信元からそこへ到達できるかは分かりません。そこで 5 台それぞれを送信元にして同じ TCP probe(TCP 接続を試して届くかを確かめること)を行い、送信元と宛先の組を表にします。

本章で受講者が適用するルールは service network(サービスを構成する VM 同士が通信するネットワーク。管理経路とは別)側だけです。目標は、操作端末から入口の HTTPS、入口から 2 台の App、2 台の App から DB というサービス経路を残し、それ以外の直接到達を止めることです。

この環境には、受講者の設定とは別の層で保たれている保護があります。本章ではその層を変更せず、実装や設定内容にも立ち入りません。受講者が作る表とルールは、サービスを構成する VM 同士の到達性だけを対象にします。

4. ファイアウォール(FW)は許可の順序と確認の位置で安全性が変わる

通信を絞るときは、必要な許可を用意する前に広い拒否を適用すると、自分の SSH 接続まで切るおそれがあります。受講者には VM console(VM の画面を直接操作する窓口)がなく、SSH を切るとその場では設定を直せません。

本章では、最初に管理接続を明示的に許可し、その後はサービス経路のルールを 1 つだけ適用するたびに、別ターミナルから新しい管理接続が成功することを確認します。最後にまとめて確認する方式にはしません。

⚠️ 接続できなくなった場合の退路: 新しい SSH 接続を確認できなかったら、その先のルールを適用しないでください。接続できなくなった場合は、演習セッションを終了すると環境が初期状態へ戻ります。

演習:TLS と 5 台の到達表で最小経路を証明する

演習環境

項目値
構築対象front01、app01、app02、db01
操作・観測端末workstation01
接続方法workstation01 から各 VM の接続名を使って SSH 接続
必要権限sudo。package 導入、証明書と秘密鍵の配置、nginx 設定、service network 側のルール設定に使う
取得経路演習環境で指定された LAN 内ミラーと教材配布元
所要時間約 90 分(STEP 1〜8 の実行)

🛟 失敗しても安全: 変更するのは演習用 VM 内の TLS 設定と service network 側のルールです。STEP 6 は 1 ルールごとに新しい SSH 接続を確認します。接続できなくなった場合は先へ進まず、演習セッションを終了すると環境が初期状態へ戻ります。

山括弧で囲んだ値は、演習環境または教材の案内に表示された値へ置き換えてください。接続先・port・interface の実値は、演習環境ごとに異なります。

4 層を clean から用意する

演習環境は毎回、素の状態から始まります。この STEP では db01、app01、app02、front01 の 4 台を、取得経路から「入口経由で要求が通る」状態まで作ります。役割ごとに 1 回だけ手順を示し、App 用の手順は 2 台へ順に適用します。構成する理由と観測の分解は第 3 章と第 4 章を参照してください。

1-1. 各 VM 共通 · 取得経路を設定する

db01 → app01 → app02 → front01 の順に、各 VM へ接続します。取得経路の設定は既存の source には触れず、章専用ディレクトリ /etc/apt/sources.list.d.part4/ だけを使います。鍵の fingerprint は使い捨ての GnuPG home で照合し、apt-get には章専用 source への限定と APT::Get::List-Cleanup=0 を渡します。この設定から package 導入までを 1 回の接続で完結させ、別のブロックへ配列を持ち越しません。

取得経路を変える前に、各 VM の package 一覧を記録します。終了時はこの一覧との差分で確認します(gpg の鍵照合は使い捨ての GnuPG home で行うため、~/.gnupg には触れません)。また、8 分類の外側に残る変更を確認するため、4 台の VM と workstation01 で、path・type・mode・uid・gid・size・mtime の sorted manifest(ファイルの場所・権限・更新日時などを一覧にしたもの)を最初に記録します。

bash · コマンド
sudo find /etc /var/lib /srv /opt /usr/local "$HOME" \
  -not -path '/var/lib/apt/*' \
  -not -path '/var/lib/dpkg/*' \
  -not -path '/var/lib/systemd/*' \
  -not -path '/var/log/*' \
  -not -path '/var/cache/*' \
  -not -name '.bash_history' \
  -not -path '/etc/passwd-' -not -path '/etc/group-' \
  -not -path '/etc/shadow-' -not -path '/etc/gshadow-' \
  -not -path '/var/lib/fwupd' -not -path '/var/lib/fwupd/*' \
  -not -path '/etc/fwupd' -not -path '/etc/fwupd/*' \
  -not -path '/var/lib/ucf' -not -path '/var/lib/ucf/*' \
  -not -path '/var/lib/landscape' -not -path '/var/lib/landscape/*' \
  -not -path '/etc/ld.so.cache' \
  -not -path '/var/lib/ubuntu-release-upgrader/release-upgrade-available' \
  -not -path '/var/lib/snapd/state.json' \
  -not -path '/var/lib/snapd/maintenance.json' \
  -not -path '/var/lib/logrotate/status' \
  -printf '%p\t%y\t%m\t%U\t%G\t%s\t%T@\n' 2>/dev/null \
  | LC_ALL=C sort \
  | sudo tee /tmp/part4-manifest-before.tsv >/dev/null
dpkg-query -W -f='${binary:Package}\t${Version}\n' | sort > /tmp/part4-package-before.tsv

1-2. DB 役割(db01)

第 4 章 STEP 01 の 1-2 と同じ手順で作ります。PostgreSQL の導入、書庫の SHA-256 照合、schema.sql の実行、App 専用ユーザと GRANT、待受アドレスのドロップイン(既存の設定ファイルを書き換えず、別のファイルで設定を足す方式)、app01・app02 の 2 行の接続許可、restart 後の SHOW・ss 確認までがひとまとまりです。

bash · コマンド
ssh deployer@db01
(
REPO_URI='http://172.16.10.22/ubuntu'
curl --fail --show-error "$REPO_URI/infra-study-archive-keyring.asc" -o /tmp/infra-study-archive-keyring.asc
GPG_HOME="$(mktemp -d)"
chmod 700 "$GPG_HOME"
EXPECTED_FINGERPRINT='089E07BDFFA26DB7C7203016297FAD1317CA1DC4'
ACTUAL_FINGERPRINT="$(gpg --homedir "$GPG_HOME" --show-keys --with-colons /tmp/infra-study-archive-keyring.asc | awk -F: '$1=="fpr" {print $10; exit}')"
test "$ACTUAL_FINGERPRINT" = "$EXPECTED_FINGERPRINT"
fingerprint_status=$?
rm -rf -- "$GPG_HOME"
test "$fingerprint_status" -eq 0
sudo install -d -m 0755 /etc/apt/keyrings /etc/apt/sources.list.d.part4
sudo install -o root -g root -m 0644 /tmp/infra-study-archive-keyring.asc /etc/apt/keyrings/infra-study-archive-keyring.asc
sudo tee /etc/apt/sources.list.d.part4/infra-study.sources >/dev/null <<EOF
Types: deb
URIs: $REPO_URI
Suites: noble
Components: part4
Architectures: amd64
Signed-By: /etc/apt/keyrings/infra-study-archive-keyring.asc
EOF
sudo chmod 0644 /etc/apt/sources.list.d.part4/infra-study.sources
APT_ONLY_PART4=(
  -o Dir::Etc::sourcelist=/dev/null
  -o Dir::Etc::sourceparts=/etc/apt/sources.list.d.part4
  -o APT::Get::List-Cleanup=0
)
sudo apt-get "${APT_ONLY_PART4[@]}" update
sudo DEBIAN_FRONTEND=noninteractive apt-get "${APT_ONLY_PART4[@]}" install -y postgresql postgresql-client

TAPAPI_ARCHIVE=/tmp/tapapi-1.1.tar.gz
TAPAPI_SHA256=8a4e518211fc661f6fdcc0b2dd45be50eb1a199b36eaeb8db1b0a985a4eb2a9e
curl --fail --show-error --location \
  --output "$TAPAPI_ARCHIVE" \
  "http://172.16.10.22/assets/part4/tapapi-1.1.tar.gz"
printf '%s  %s\n' "$TAPAPI_SHA256" "$TAPAPI_ARCHIVE" \
  | sha256sum --check - || { echo 'SHA-256 mismatch: stop' >&2; exit 1; }
TAPAPI_TMP="$(mktemp -d)"
tar -xzf "$TAPAPI_ARCHIVE" -C "$TAPAPI_TMP"

sudo -u postgres createuser --login tap_app
sudo -u postgres psql -c '\password tap_app'
sudo -u postgres createdb transit_lab
sudo -u postgres psql -d transit_lab < "$TAPAPI_TMP/tapapi/schema.sql"
sudo -u postgres psql -d transit_lab <<'SQL'
GRANT CONNECT ON DATABASE transit_lab TO tap_app;
GRANT USAGE ON SCHEMA public TO tap_app;
GRANT SELECT, UPDATE ON cards TO tap_app;
GRANT INSERT ON tap_events TO tap_app;
SQL
rm -rf "$TAPAPI_TMP"

PG_CONF="$(sudo -u postgres psql -Atc 'SHOW config_file;')"
PG_HBA="$(sudo -u postgres psql -Atc 'SHOW hba_file;')"
PG_CONF_DIR="$(dirname "$PG_CONF")"

sudo install -d -m 0755 "$PG_CONF_DIR/conf.d"
sudo tee "$PG_CONF_DIR/conf.d/part4.conf" > /dev/null <<'EOF'
listen_addresses = '172.16.10.24'
EOF
sudo grep -nE "^[[:space:]]*include_dir[[:space:]]*=[[:space:]]*'conf\.d'" "$PG_CONF"
for APP_ADDR in 172.16.10.20 172.16.10.21; do
  HBA_LINE="host transit_lab tap_app ${APP_ADDR}/32 scram-sha-256"
  sudo grep -qsF "$HBA_LINE" "$PG_HBA" || \
    printf '%s\n' "$HBA_LINE" | sudo tee -a "$PG_HBA" > /dev/null
done
sudo systemctl restart postgresql
sudo -u postgres psql -Atc 'SHOW listen_addresses;'
sudo ss -lntp | grep postgres
)
rc=$?
((rc == 0)) || echo 'SSH 接続は維持したまま処理を中止しました' >&2
期待される出力
(2026-09-02 実測)
=== schema ===
CREATE TABLE
CREATE TABLE
INSERT 0 1

=== GRANT ===
GRANT
GRANT
GRANT
GRANT

=== include_dir の確認 ===
817:include_dir = 'conf.d'			# include files ending in '.conf' from

=== pg_hba への追記(2 行)===
133:host transit_lab tap_app 172.16.10.20/32 scram-sha-256
134:host transit_lab tap_app 172.16.10.21/32 scram-sha-256

=== restart 後の待受 ===
172.16.10.24
LISTEN 0      200     172.16.10.24:5432      0.0.0.0:*    users:(("postgres",pid=3752,fd=6))

SHOW listen_addresses(listen_addresses は DB が接続を待つアドレスの設定)が 172.16.10.24 を返し、ss の待受も同じアドレスだけになっていることを 2 つの出力で確かめます。片方だけでは足りません。設定値が読めても、実際にその socket で 待っているとは限らないからです。

1-3. App 役割(app01 → app02 の順)

次の 1 組を app01、app02 の順に適用します。配置、所有者、権限、DB 接続項目は共通にし、環境ファイルの TAPAPI_NODE だけを対象名へ変えます。SHA-256 が一致しなければ、その場で中止します。

まず対象の App へ接続します(1 台目は app01、終わったら exit して 2 台目は app02)。

bash · コマンド
ssh deployer@<app01の接続名>

続いて、接続した VM の中で次のブロックを実行します。

bash · コマンド
# app01 で一度実行し、次に app02 でも同じブロックを実行する
(
REPO_URI='http://172.16.10.22/ubuntu'
curl --fail --show-error "$REPO_URI/infra-study-archive-keyring.asc" -o /tmp/infra-study-archive-keyring.asc
GPG_HOME="$(mktemp -d)"
chmod 700 "$GPG_HOME"
EXPECTED_FINGERPRINT='089E07BDFFA26DB7C7203016297FAD1317CA1DC4'
ACTUAL_FINGERPRINT="$(gpg --homedir "$GPG_HOME" --show-keys --with-colons /tmp/infra-study-archive-keyring.asc | awk -F: '$1=="fpr" {print $10; exit}')"
test "$ACTUAL_FINGERPRINT" = "$EXPECTED_FINGERPRINT"
fingerprint_status=$?
rm -rf -- "$GPG_HOME"
test "$fingerprint_status" -eq 0
sudo install -d -m 0755 /etc/apt/keyrings /etc/apt/sources.list.d.part4
sudo install -o root -g root -m 0644 /tmp/infra-study-archive-keyring.asc /etc/apt/keyrings/infra-study-archive-keyring.asc
sudo tee /etc/apt/sources.list.d.part4/infra-study.sources >/dev/null <<EOF
Types: deb
URIs: $REPO_URI
Suites: noble
Components: part4
Architectures: amd64
Signed-By: /etc/apt/keyrings/infra-study-archive-keyring.asc
EOF
sudo chmod 0644 /etc/apt/sources.list.d.part4/infra-study.sources
APT_ONLY_PART4=(
  -o Dir::Etc::sourcelist=/dev/null
  -o Dir::Etc::sourceparts=/etc/apt/sources.list.d.part4
  -o APT::Get::List-Cleanup=0
)
sudo apt-get "${APT_ONLY_PART4[@]}" update
sudo DEBIAN_FRONTEND=noninteractive apt-get "${APT_ONLY_PART4[@]}" install -y python3-psycopg2

TAPAPI_ARCHIVE=/tmp/tapapi-1.1.tar.gz
TAPAPI_SHA256=8a4e518211fc661f6fdcc0b2dd45be50eb1a199b36eaeb8db1b0a985a4eb2a9e
curl --fail --show-error --location \
  --output "$TAPAPI_ARCHIVE" \
  "http://172.16.10.22/assets/part4/tapapi-1.1.tar.gz"
printf '%s  %s\n' "$TAPAPI_SHA256" "$TAPAPI_ARCHIVE" \
  | sha256sum --check - || { echo 'SHA-256 mismatch: stop' >&2; exit 1; }
test "$(stat -c %s "$TAPAPI_ARCHIVE")" -eq 2728
tar -tzf "$TAPAPI_ARCHIVE"

TAPAPI_TMP="$(mktemp -d)"
tar -xzf "$TAPAPI_ARCHIVE" -C "$TAPAPI_TMP"
id tapapi >/dev/null 2>&1 || \
  sudo useradd --system --home-dir /opt/tapapi --shell /usr/sbin/nologin tapapi
sudo install -d -o root -g root -m 0755 /opt/tapapi /etc/tapapi
sudo install -o root -g root -m 0755 "$TAPAPI_TMP/tapapi/tapapi.py" /opt/tapapi/tapapi.py
sudo install -o root -g root -m 0644 "$TAPAPI_TMP/tapapi/schema.sql" /opt/tapapi/schema.sql
sudo install -o root -g root -m 0644 "$TAPAPI_TMP/tapapi/tapapi.service" /etc/systemd/system/tapapi.service
sudo install -o root -g tapapi -m 0640 "$TAPAPI_TMP/tapapi/tapapi.env.example" /etc/tapapi/tapapi.env
rm -rf "$TAPAPI_TMP"
sudoedit /etc/tapapi/tapapi.env
sudo systemctl daemon-reload
sudo systemctl start tapapi.service
systemctl status tapapi.service --no-pager
# 待受が開くまで待つ(開かなければ 30 秒で諦める)
timeout 30 bash -c 'until curl -sf http://localhost:8080/health > /dev/null; do sleep 1; done'
curl --fail --show-error http://localhost:8080/health
)
rc=$?
((rc == 0)) || echo 'SSH 接続は維持したまま処理を中止しました' >&2

環境ファイルには TAPAPI_NODE を接続中の VM 名(app01 または app02)に合わせ、PGHOST=172.16.10.24、PGPORT=5432、PGDATABASE=transit_lab、PGUSER=tap_app は 2 台で同じ値にします。秘密値は端末出力や教材の証跡へ貼りません。各台の /health(App 自身が状態を答える確認用の URL)が "db":"ok" を返すことが、この役割の完了条件です。

期待される出力
(2026-09-02 実測) app01。app02 は `TAPAPI_NODE` だけが異なります。
=== 依存 package ===
python3-psycopg2 2.9.9-1build1

=== 書庫の照合 ===
/tmp/tapapi-1.1.tar.gz: OK
size=2728 OK
tapapi/
tapapi/schema.sql
tapapi/tapapi.env.example
tapapi/tapapi.py
tapapi/tapapi.service

=== 非機密の環境設定(秘密値は伏せる)===
TAPAPI_NODE=app01
PGHOST=172.16.10.24
PGPORT=5432
PGDATABASE=transit_lab
PGUSER=tap_app
/etc/tapapi/tapapi.env owner=root group=tapapi mode=640

=== service 状態 ===
* tapapi.service - Part 4 transit-card tap API
     Loaded: loaded (/etc/systemd/system/tapapi.service; disabled; preset: enabled)
     Active: active (running) since Wed 2026-09-02 10:34:50 JST; 2s ago
   Main PID: 2029 (python3)

=== /health ===
{"status":"ok","db":"ok","node":"app01"}     ← app02 では "node":"app02"

2 台で違うのは TAPAPI_NODE の 1 項目だけです。/health の "db":"ok" が、 その App から DB へ実際に繋がったことの確認になります。

1-4. 入口役割(front01)

nginx を導入し、第 4 章 STEP 04 と同じ正常時の振り分け(upstream:転送先の一覧に 2 台を登録)を設定します。受動的な故障検出の値は本章では使いません。

bash · コマンド
ssh deployer@front01
REPO_URI='http://172.16.10.22/ubuntu'
curl --fail --show-error "$REPO_URI/infra-study-archive-keyring.asc" -o /tmp/infra-study-archive-keyring.asc
GPG_HOME="$(mktemp -d)"
chmod 700 "$GPG_HOME"
EXPECTED_FINGERPRINT='089E07BDFFA26DB7C7203016297FAD1317CA1DC4'
ACTUAL_FINGERPRINT="$(gpg --homedir "$GPG_HOME" --show-keys --with-colons /tmp/infra-study-archive-keyring.asc | awk -F: '$1=="fpr" {print $10; exit}')"
test "$ACTUAL_FINGERPRINT" = "$EXPECTED_FINGERPRINT"
fingerprint_status=$?
rm -rf -- "$GPG_HOME"
test "$fingerprint_status" -eq 0
sudo install -d -m 0755 /etc/apt/keyrings /etc/apt/sources.list.d.part4
sudo install -o root -g root -m 0644 /tmp/infra-study-archive-keyring.asc /etc/apt/keyrings/infra-study-archive-keyring.asc
sudo tee /etc/apt/sources.list.d.part4/infra-study.sources >/dev/null <<EOF
Types: deb
URIs: $REPO_URI
Suites: noble
Components: part4
Architectures: amd64
Signed-By: /etc/apt/keyrings/infra-study-archive-keyring.asc
EOF
sudo chmod 0644 /etc/apt/sources.list.d.part4/infra-study.sources
APT_ONLY_PART4=(
  -o Dir::Etc::sourcelist=/dev/null
  -o Dir::Etc::sourceparts=/etc/apt/sources.list.d.part4
  -o APT::Get::List-Cleanup=0
)
sudo apt-get "${APT_ONLY_PART4[@]}" update
sudo DEBIAN_FRONTEND=noninteractive apt-get "${APT_ONLY_PART4[@]}" install -y nginx

章専用の設定を作ります。

nginx · 設定
log_format tap_with_upstream
    '$remote_addr $request status=$status event_id=$http_x_event_id '
    'upstream=$upstream_addr upstream_status=$upstream_status';

upstream tap_app_backend {
    server <app01の接続名>:8080;
    server <app02の接続名>:8080;
}

server {
    listen <入口の待受port>;
    access_log /var/log/nginx/tap_access.log tap_with_upstream;

    location / {
        proxy_set_header X-Event-ID $http_x_event_id;
        proxy_set_header Host $host;
        proxy_pass http://tap_app_backend;
    }
}
bash · コマンド
sudoedit "<nginxの章専用設定ファイル>"
sudo ln -s "<nginxの章専用設定ファイル>" "<nginxの有効化先>"
sudo nginx -t
sudo systemctl reload nginx
systemctl status nginx --no-pager
期待される出力
(2026-09-02 実測)
=== 導入 ===
nginx version: nginx/1.24.0 (Ubuntu)
nginx 1.24.0-2ubuntu7.15

=== 有効化先 ===
lrwxrwxrwx 1 root root 34 default  -> /etc/nginx/sites-available/default
lrwxrwxrwx 1 root root 35 tap.conf -> /etc/nginx/sites-available/tap.conf

=== nginx -t ===
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

=== reload 後の待受 ===
LISTEN 0      511          0.0.0.0:8080      0.0.0.0:*    users:(("nginx",...))
LISTEN 0      511          0.0.0.0:80        0.0.0.0:*    users:(("nginx",...))

⚠️ 既定サイトが残ります:Ubuntu の nginx は sites-enabled/default(port 80)が最初から有効です。 章専用設定は別の port なので演習は成立しますが、入口には教材が意図していない待受が 1 つあります。 STEP 6 で「最小に絞る」ときは、この既定サイトをどう扱うかも決めてください。

1-5. 成立ゲート · 入口経由で 4 層を貫通する

workstation01 から、入口経由で /health を要求します。教材アプリの GET(読み取るだけの要求の種類)は /health だけを受け付け、入口はすべての path(URL のうち場所を示す部分)を App へ転送するため、この 1 回の要求で「入口 → App → DB」の 3 つの接続が同時に検査されます。

bash · コマンド
# workstation01 で実行する
curl --fail --show-error "http://<front01の接続名>:<入口の待受port>/health"

応答が {"status":"ok","db":"ok","node":...} であれば、絞る前の 4 層は成立しています。node の値は振り分け先によって app01 にも app02 にもなります。どちらでも正常です。

期待される出力
(2026-09-02 実測) 振り分けの両方を示すため 4 回続けて実行。
{"status":"ok","db":"ok","node":"app01"}
{"status":"ok","db":"ok","node":"app02"}
{"status":"ok","db":"ok","node":"app01"}
{"status":"ok","db":"ok","node":"app02"}

1 回の要求で「入口 → App → DB」の 3 接続が同時に検査されます。node が交互に変わるのは、 入口が 2 台へ振り分けているためで、どちらが返っても正常です。

5 台すべてで時刻を確認します。証明書の期限判定に使うため、大きなずれがある場合は変更せず、演習環境の案内へ戻ってください。

bash · コマンド
date --iso-8601=seconds
期待される出力
(2026-09-02 実測)
workstation01  2026-09-02T10:35:42+09:00
front01        2026-09-02T01:35:44+00:00
learner01      2026-09-02T10:35:45+09:00
learner02      2026-09-02T10:35:46+09:00
db01           2026-09-02T01:35:47+00:00

🔴 表示のタイムゾーンが 5 台で揃っていません。上の実測では front01 と db01 だけが UTC、 残る 3 台は JST です。時刻そのものはずれていません(10:35 JST = 01:35 UTC)。 ずれているように見えたら、まずタイムゾーンの表示か時刻そのものかを分けてください。 certificate の notBefore / notAfter は UTC で書かれているので、 比較したいときは date --iso-8601=seconds --utc に揃えるのが確実です。

絞る前の到達性の表を作る

この STEP の表が、STEP 7 の比較基準です。先に保存しなければ、変更後に「もともと届かなかった」のか「ルールで絞れた」のかを区別できません。

まず各サービス VM で待受 port を列挙し、どのプロセスが公開面を持つかを記録します。

bash · コマンド
sudo ss -lntp
期待される出力
(2026-09-02 実測) 実アドレスは対象 port の特定に要る範囲だけ示します。
--- front01 ---
LISTEN 0  511  0.0.0.0:8080  nginx     ← 入口(章専用設定)
LISTEN 0  511  0.0.0.0:80    nginx     ← 既定サイト
LISTEN 0 4096  0.0.0.0:22    sshd
--- app01 / app02 ---
LISTEN 0    5  0.0.0.0:8080  python3   ← App service
LISTEN 0 4096  0.0.0.0:22    sshd
--- db01 ---
LISTEN 0  200  172.16.10.24:5432  postgres   ← VLAN10 のアドレスだけで待つ
LISTEN 0 4096  0.0.0.0:22         sshd
対象port待受の範囲
入口 HTTPS8443この時点ではまだ無い(STEP 4 で追加)
入口 HTTP8080すべての interface
App service8080すべての interface
DB5432172.16.10.24 だけ

DB だけが待受アドレスを絞っている点に注目してください。STEP 2 の表で db01 から db-service が DENY になるのは、この設定の当然の結果です。

次の同一コマンド群を workstation01、front01、app01、app02、db01 の順に実行します。成功・失敗のどちらでも全行を残すため、途中で処理を止めません。

ここでは名前解決と TCP 到達性を分けて判定します。getent で接続名を IPv4 アドレスへ解決できなければ RESOLVE_DENY、解決後のアドレスへ TCP 接続できなければ DENY です。これにより、名前解決側の異常をファイアウォールの拒否と取り違えません。

bash · コマンド
probe_tcp() {
  TARGET="$1"
  PORT="$2"
  LABEL="$3"
  TARGET_IP="$(getent ahostsv4 "$TARGET" | awk 'NR == 1 { print $1 }')"
  if [ -z "$TARGET_IP" ]; then
    printf '%s\t%s\tRESOLVE_DENY\n' "$(hostname)" "${LABEL}"
  elif timeout 3 bash -c ": </dev/tcp/${TARGET_IP}/${PORT}" 2>/dev/null; then
    printf '%s\t%s\tALLOW\n' "$(hostname)" "${LABEL}"
  else
    printf '%s\t%s\tDENY\n' "$(hostname)" "${LABEL}"
  fi
}

PHASE=before
{
  probe_tcp "<front01の接続名>" "<HTTPの待受port>" "front-http"
  probe_tcp "<front01の接続名>" "<HTTPSの待受port>" "front-https"
  probe_tcp "<app01の接続名>" "<Appの待受port>" "app01-service"
  probe_tcp "<app02の接続名>" "<Appの待受port>" "app02-service"
  probe_tcp "<db01の接続名>" "<DBの待受port>" "db-service"
} | tee "/tmp/ch5-reach-${PHASE}-$(hostname).tsv"

各ホストの結果を、同じ列順の TSV ファイルへ保存します。

bash · コマンド
# workstation01 で 5 台ぶんを 1 つの表にまとめる
PHASE=before
OUT="/tmp/ch5-reachability-${PHASE}.tsv"
cat "/tmp/ch5-reach-${PHASE}-$(hostname).tsv" > "$OUT"
for H in front01 172.16.10.20 172.16.10.21 db01; do
  ssh deployer@"$H" "cat /tmp/ch5-reach-${PHASE}-\$(hostname).tsv" >> "$OUT"
done
sort -o "$OUT" "$OUT"
cat "$OUT"

各台で date --iso-8601=seconds も記録し、変更前と変更後を同じ順序・同じ列で採ったことを示せるようにします。

期待される出力
workstation01	front-http	ALLOW
workstation01	front-https	DENY
workstation01	app01-service	ALLOW
workstation01	app02-service	ALLOW
workstation01	db-service	ALLOW
front01	front-http	ALLOW
front01	front-https	DENY
front01	app01-service	ALLOW
front01	app02-service	ALLOW
front01	db-service	ALLOW
learner01	front-http	ALLOW
learner01	front-https	DENY
learner01	app01-service	ALLOW
learner01	app02-service	ALLOW
learner01	db-service	ALLOW
learner02	front-http	ALLOW
learner02	front-https	DENY
learner02	app01-service	ALLOW
learner02	app02-service	ALLOW
learner02	db-service	ALLOW
db01	front-http	ALLOW
db01	front-https	DENY
db01	app01-service	ALLOW
db01	app02-service	ALLOW
db01	db-service	DENY

db01 自身の db-service が DENY なのは、接続名が loopback を指す一方、PostgreSQL はサービス用アドレスだけで待ち受けているためです。ファイアウォール適用前の拒否として、測定値をそのまま残します。

変更前の表は次の形へまとめます。セルは実測した ALLOW または DENY で埋め、予想で書きません。

送信元front HTTPfront HTTPSapp01 serviceapp02 serviceDB service
workstation01ALLOWDENYALLOWALLOWALLOW
front01ALLOWDENYALLOWALLOWALLOW
app01ALLOWDENYALLOWALLOWALLOW
app02ALLOWDENYALLOWALLOWALLOW
db01ALLOWDENYALLOWALLOWDENY

表の完成時点では、結果を理想形に直しません。現在の公開面をそのまま保存することが目的です。

演習用 CA と server certificate を作って読む

この STEP では、秘密鍵を作る場所と、certificate に署名する場所を分けます。server の秘密鍵は front01 で作り、front01 の外へ一度も出しません。workstation01 へ渡すのは CSR(署名要求)だけです。CSR には公開鍵と名前しか入っておらず、秘密鍵は含まれません。

📔 コラム · 秘密鍵は運ばないのが原則 秘密鍵を安全に運ぶ方法を考えるより、運ばなくて済む置き方を選ぶほうが確実です。運ばなければ、転送中に盗まれることも、端末の履歴やスクロールバックに残ることもありません。この章では「配布物は公開してよいものだけ」という形を最初から取ります。

まず workstation01 で演習用 CA の秘密鍵と certificate を作ります。秘密鍵を表示したり、章の証跡へ貼ったりしないでください。

bash · コマンド
mkdir -p "$HOME/ch5-tls"
chmod 700 "$HOME/ch5-tls"
cd "$HOME/ch5-tls"

openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out lab-ca.key
chmod 600 lab-ca.key
openssl req -x509 -new -sha256 \
  -key lab-ca.key \
  -days 7 \
  -subj "/CN=<演習用CA名>" \
  -out lab-ca.crt

次に front01 へ接続し、server の秘密鍵と CSR を front01 上で作ります。秘密鍵は最初から最後まで front01 に置いたままにします。

bash · コマンド
ssh deployer@front01
sudo install -d -m 700 /etc/nginx/tls
sudo openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 \
  -out /etc/nginx/tls/front01.key
sudo chmod 600 /etc/nginx/tls/front01.key
sudo openssl req -new -sha256 \
  -key /etc/nginx/tls/front01.key \
  -subj "/CN=<front01のTLS接続名>" \
  -out /tmp/front01.csr
期待される出力
(2026-09-02 実測) 秘密鍵の内容は表示しません。
/etc/nginx/tls/front01.key owner=root group=root mode=600 size=1708
/tmp/front01.csr           owner=root group=root mode=644 size=887

Certificate request self-signature verify OK
subject=CN = front01

秘密鍵は mode=600・所有者 root で、この VM の外へ出す手順がどこにもありません。 一方 CSR は mode=644 で、公開してよい情報だけが入っています。

CSR を workstation01 へ取り出し、中身を読んでから署名へ進みます。取り出すのは CSR だけです。

bash · コマンド
scp deployer@<front01の接続名>:/tmp/front01.csr "$HOME/ch5-tls/front01.csr"
openssl req -in "$HOME/ch5-tls/front01.csr" -noout -subject -verify
期待される出力
Certificate request self-signature verify OK
subject=CN = front01

SAN を拡張ファイルへ記載し、演習用 CA で署名します。

bash · コマンド
cd "$HOME/ch5-tls"
cat > front01.ext <<'EOF'
subjectAltName=DNS:<front01のTLS接続名>
keyUsage=digitalSignature,keyEncipherment
extendedKeyUsage=serverAuth
EOF

openssl x509 -req -sha256 \
  -in front01.csr \
  -CA lab-ca.crt \
  -CAkey lab-ca.key \
  -CAcreateserial \
  -days 7 \
  -extfile front01.ext \
  -out front01.crt

作成後は SAN、期限、発行者、subject(証明書の持ち主を示す名前)を読みます。秘密鍵ではなく certificate を入力にします。

bash · コマンド
cd "$HOME/ch5-tls"
openssl x509 -in front01.crt -noout \
  -subject -issuer -dates -ext subjectAltName
openssl verify -CAfile lab-ca.crt front01.crt
期待される出力
subject=CN = infra-study-lab-ca
notBefore=Sep  1 13:11:21 2026 GMT
notAfter=Sep  8 13:11:21 2026 GMT
期待される出力
Certificate request self-signature verify OK
subject=CN = front01
subject=CN = front01
issuer=CN = infra-study-lab-ca
notBefore=Sep  1 13:11:55 2026 GMT
notAfter=Sep  8 13:11:55 2026 GMT
X509v3 Subject Alternative Name:
    DNS:front01
front01.crt: OK

ここで、接続に使う名前が SAN にあり、発行者が演習用 CA で、有効期間が現在時刻を含むことを指差し確認します。どれか 1 つでも違えば入口へ配置しません。

front01 に certificate を配置する

TLS 用ディレクトリと秘密鍵は STEP 3 で front01 に作ってあります。ここで配置するのは certificate だけです。秘密鍵を転送する手順はありません。

bash · コマンド
# workstation01 から certificate を渡す(certificate は公開してよい情報)
scp "$HOME/ch5-tls/front01.crt" deployer@<front01の接続名>:/tmp/front01.crt
bash · コマンド
ssh deployer@front01
sudo install -m 644 /tmp/front01.crt /etc/nginx/tls/front01.crt
sudo stat /etc/nginx/tls/front01.crt /etc/nginx/tls/front01.key
期待される出力
(2026-09-02 実測)
/etc/nginx/tls/front01.crt owner=root group=root mode=644 size=1172
/etc/nginx/tls/front01.key owner=root group=root mode=600 size=1708

certificate は読める権限(644)、秘密鍵は所有者だけ(600)です。同じディレクトリに置いても 権限を変えていることを確認してください。

配置した certificate が、front01 にある秘密鍵と対になっていることを確認します。秘密鍵と certificate を別々の場所で作ったので、組が合っていることを自分で確かめてから nginx へ渡します。

bash · コマンド
sudo openssl x509 -noout -modulus -in /etc/nginx/tls/front01.crt | openssl sha256
sudo openssl rsa  -noout -modulus -in /etc/nginx/tls/front01.key | openssl sha256
期待される出力
(2026-09-02 実測) modulus そのものは表示せず、ハッシュだけを比べます。
crt: SHA2-256(stdin)= bf02321de9e2d48a92a0a530179903c45c13ee9992644e6b92965daac1c1cf8c
key: SHA2-256(stdin)= bf02321de9e2d48a92a0a530179903c45c13ee9992644e6b92965daac1c1cf8c

2 行が完全に一致すれば、この certificate はこの秘密鍵に対応しています。 一致しなければ nginx へ渡してはいけません(起動はしても TLS が成立しません)。

章専用の nginx 設定へ、HTTP から HTTPS への転送と TLS 待受を追加します。upstream と API(プログラム同士が要求と応答をやり取りする窓口)の設定は STEP 1 で用意したものを参照します。

nginx · 設定
server {
    listen <HTTPの待受port>;
    server_name <front01のTLS接続名>;
    return 301 https://$host:<HTTPSの待受port>$request_uri;
}

server {
    listen <HTTPSの待受port> ssl;
    server_name <front01のTLS接続名>;

    ssl_certificate     /etc/nginx/tls/front01.crt;
    ssl_certificate_key /etc/nginx/tls/front01.key;

    location / {
        proxy_set_header X-Event-ID $http_x_event_id;
        proxy_set_header Host $host;
        proxy_pass http://tap_app_backend;
    }
}

構文検査が成功した場合だけ reload します。reload 後は待受と提示 certificate を別々に確認します。

bash · コマンド
sudoedit "<nginxの章専用設定ファイル>"
sudo nginx -t
sudo systemctl reload nginx
systemctl status nginx --no-pager
sudo ss -lntp | grep nginx
openssl s_client \
  -connect "<front01の接続名>:<HTTPSの待受port>" \
  -servername "<front01のTLS接続名>" \
  -showcerts </dev/null
期待される出力
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
LISTEN 0      511          0.0.0.0:8443      0.0.0.0:*    users:(("nginx",...))
期待される出力
(2026-09-02 実測)
=== nginx -t ===
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

=== reload 後の待受 ===
LISTEN 0      511          0.0.0.0:8443      0.0.0.0:*    users:(("nginx",...))
LISTEN 0      511          0.0.0.0:8080      0.0.0.0:*    users:(("nginx",...))
LISTEN 0      511          0.0.0.0:80        0.0.0.0:*    users:(("nginx",...))

=== 提示された certificate ===
subject=CN = front01
issuer=CN = infra-study-lab-ca

HTTPS の待受(8443)が増えたことと、提示された certificate が STEP 3 で署名したものであることを、 別々の出力で確かめます。

この時点では HTTP の転送も構成されています。STEP 6 でクライアントから許可する入口を HTTPS だけへ絞った後は、service network 側の HTTP 転送へ直接到達できないことも表で確認します。

信頼前の失敗と信頼後の成功を比較する

workstation01 から、まず演習用 CA を指定せずに HTTPS へ接続します。失敗は予定された観測結果なので、終了 status を保存します。

bash · コマンド
curl --show-error --verbose \
  "https://<front01のTLS接続名>:<HTTPSの待受port>/health"
printf 'curl_exit=%s\n' "$?"
期待される出力
(2026-10-12 実測・抜粋。--verbose の経過行〔* > < で始まる行〕は省略)
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. ...
curl_exit=60

次に、サーバが提示する chain と検証結果を読みます。

bash · コマンド
openssl s_client \
  -connect "<front01の接続名>:<HTTPSの待受port>" \
  -servername "<front01のTLS接続名>" \
  -showcerts </dev/null
期待される出力
(2026-09-02 実測)
=== 信頼前(演習用 CA を渡さない)===
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it.
http=000
curl_exit=60

=== 提示された chain と検証結果 ===
depth=0 CN = front01
subject=CN = front01
issuer=CN = infra-study-lab-ca
Verification error: unable to verify the first certificate

chain は 1 段だけ(depth=0)で、その発行者は演習用 CA です。 失敗の理由は「暗号化できない」ではなく、その CA を信頼する材料を client が持っていないことです。

演習用 CA を --cacert で明示し、同じ URL へ再接続します。OS 全体の信頼ストア(信頼する CA の証明書を集めた置き場)は変更しません。

bash · コマンド
curl --fail --show-error --verbose \
  --cacert "$HOME/ch5-tls/lab-ca.crt" \
  "https://<front01のTLS接続名>:<HTTPSの待受port>/health"
期待される出力
(2026-09-02 実測)
{"status":"ok","db":"ok","node":"app02"}
http=200

Verify return code: 0 (ok)

渡したのは CA の certificate 1 本だけです。OS 全体の信頼ストアは変更していません。 同じ URL・同じ接続名で、結果だけが変わったことを確認してください。

信頼前後で変わったのは、通信を暗号化できるかではなく、提示された certificate を信頼できる相手のものとして検証できるかです。成功時も SAN、発行者、期限のどれを満たしたかを説明してください。

サービス側の経路を 1 ルールずつ最小に絞る

⚠️ 最後にまとめて確認しない: 受講者には VM console がありません。各ルールを 1 つ適用するたびに、別ターミナルから対象 VM への新しい SSH 接続を確認します。失敗したら次のルールへ進みません。

この章では ufw(簡単な書き方でファイアウォールを設定する道具)を使います。演習環境の 4 台には ufw / nft / iptables(Linux の通信制御を操作する道具)がいずれも入っていますが、初期状態では ufw が inactive で、ルールは 1 行もありません。つまり、これから受講者が書くルールが、この層で最初のルールになります。

ufw の既定は 受信は拒否・送信は許可です。有効化した瞬間から、許可を書いていない受信はすべて落ちます。管理接続(SSH)の許可を先に置いてから有効化するのはこのためです。

⚠️ 順序を逆にすると戻れません: 許可を置く前に有効化すると、その瞬間に自分の SSH が落ちます。受講者には VM console がないため、演習セッションを終了して環境を初期状態へ戻す以外に復旧手段がありません。

まず workstation01 に制御用ターミナルを 1 つ残し、4 台のサービス VM への新しい SSH 接続が成功する初期状態を保存します。あわせて、各 VM で ufw が inactive であることと interface の一覧を記録します。

bash · コマンド
# 対象 VM 内で初期状態を記録する
sudo ufw status verbose
ip -br addr show
期待される出力
(2026-09-02 実測) 実アドレスは VLAN10 側だけを示します。
--- front01 ---
Status: inactive
eth0             UP             <管理LANのアドレス>
ens19.10@ens19   UP             172.16.10.23/24
--- app01 ---
Status: inactive
ens18            UP             <管理LANのアドレス>
ens20.10@ens20   UP             172.16.10.20/24
--- app02 ---
Status: inactive
ens18            UP             <管理LANのアドレス>
ens20.10@ens20   UP             172.16.10.21/24
--- db01 ---
Status: inactive
eth0             UP             <管理LANのアドレス>
ens19.10@ens19   UP             172.16.10.24/24

4 台とも Status: inactive で、ルールは 1 行もありません。 これから書くルールが、この層で最初のルールになります。

bash · コマンド
for HOST in front01 172.16.10.20 172.16.10.21 db01; do
  ssh -o BatchMode=yes -o ConnectTimeout=5 "deployer@${HOST}" \
    'printf "management-ok host=%s time=%s\n" "$(hostname)" "$(date --iso-8601=seconds)"'
done
期待される出力
(2026-09-02 実測)
management-ok host=front01   time=2026-09-02T01:43:34+00:00
management-ok host=learner01 time=2026-09-02T10:43:35+09:00
management-ok host=learner02 time=2026-09-02T10:43:35+09:00
management-ok host=db01      time=2026-09-02T01:43:35+00:00

4 台すべてで成功することを、ルールを 1 つも入れる前に確かめます。 これが後の比較基準になります(失敗したときに「元から届いていなかった」を排除できます)。

6-1. 管理接続の許可を先に置く

対象 VM は 1 台ずつ扱います。最初の VM に管理接続の許可ルールを 1 つだけ登録してから有効化し、直後に制御用ターミナルからその VM へ新しく SSH 接続します。成功を記録してから次の VM へ移ります。

ufw は inactive の間もルールを登録できます。登録が先、有効化が後です。

bash · コマンド
# 対象 VM 内で 1 ルールだけ登録してから有効化する
sudo ufw allow from <workstation01の接続元> to any port <管理接続のport> proto tcp
sudo ufw --force enable
sudo ufw status verbose

# 直後に workstation01 の別ターミナルから確認する
ssh -o BatchMode=yes -o ConnectTimeout=5 deployer@<対象VMの接続名> \
  'printf "management-ok host=%s time=%s\n" "$(hostname)" "$(date --iso-8601=seconds)"'

この「1 ルール適用 → 新しい SSH 接続」の対を front01、app01、app02、db01 の順に繰り返します。4 台分を先にまとめて適用してはいけません。

期待される出力
(2026-09-02 実測) 4 台とも同じ形。以下は 1 台目(front01)。
Rules updated
Firewall is active and enabled on system startup
Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), disabled (routed)
New profiles: skip

To                         Action      From
--                         ------      ----
22/tcp                     ALLOW IN    172.16.10.22

--- 直後の新しい SSH 接続 ---
management-ok host=front01 time=2026-09-02T01:43:58+00:00

4 台すべてで「1 ルール登録 → 有効化 → 直後に新しい SSH が成功」を確認しました。 Default: deny (incoming) になった後も管理接続が生きているのは、先に許可を置いたからです。

6-2. workstation01 から front01 の HTTPS を許可する

front01 に、操作端末から入口 HTTPS への許可を 1 つだけ適用します。直後に front01 への新しい SSH 接続を確認し、続けて HTTPS を試します。

🔴 この時点で HTTPS はまだ通りません。入口までは届きますが、入口から App への経路をまだ開けていないので応答が返らないためです。通らないことを確認するのがこの手順の目的です。「入口までは届いている」ことだけを見たいときは、TLS の成立(Verify return code: 0)を確かめてください。

📔 なぜ、通らないタイミングで測るのか 通るようになってから測ると、観測できるのは「通った」だけです。いまのように 1 段ずつ開けていく途中で測ると、「入口までは届く」「TLS は成立する」「App までは届かない」を層ごとに分けて確認できます。 障害の切り分けは、まさにこの順で見ます。どこまで届いているかを層で言えることが、「なんとなく繋がらない」と「App の手前で止まっている」を分ける力になります。 クラウドのマネージド型の入口では、この「絞った途中の状態」自体を作れません。実機だから見られる景色です。

bash · コマンド
sudo ufw allow from <workstation01の接続元> to any port <HTTPSの待受port> proto tcp
sudo ufw status numbered
bash · コマンド
ssh -o BatchMode=yes -o ConnectTimeout=5 deployer@front01 \
  'printf "management-ok host=%s time=%s\n" "$(hostname)" "$(date --iso-8601=seconds)"'
curl --fail --show-error --max-time 10 \
  --cacert "$HOME/ch5-tls/lab-ca.crt" \
  -o /dev/null -w 'http=%{http_code}\n' \
  "https://<front01のTLS接続名>:<HTTPSの待受port>/health"
期待される出力
(2026-09-02 実測)
=== ルール ===
[ 1] 22/tcp                     ALLOW IN    172.16.10.22
[ 2] 8443/tcp                   ALLOW IN    172.16.10.22

=== 直後の管理接続 ===
management-ok host=front01 time=2026-09-02T01:45:12+00:00

=== 直後の HTTPS 応答 ===
curl: (28) Operation timed out after 10002 milliseconds with 0 bytes received
http=000

🔴 この時点では HTTPS は通りません。成功しないのが正しい観測結果です。 6-1 で 4 台とも deny (incoming) にしたため、入口から App への経路がまだ塞がっています。 TLS の終端まではできますが、入口が App に届かないので応答が返りません。 確認できるのは「入口までは届く」ところまでです。TLS が成立していることだけを見たいなら、 openssl s_client で Verify return code: 0 (ok) を確かめてください。 HTTPS 経由でアプリの応答まで通るのは、6-6 まで終えた後です(そこで改めて確認します)。

6-3. front01 から app01 を許可する

app01 に、front01 から App service への許可を 1 つだけ適用します。直後に app01 への新しい SSH 接続を確認し、front01 から App service へ probe します。

bash · コマンド
sudo ufw allow from <front01の接続元> to any port <Appの待受port> proto tcp
sudo ufw status numbered
bash · コマンド
ssh -o BatchMode=yes -o ConnectTimeout=5 deployer@<app01の接続名> \
  'printf "management-ok host=%s time=%s\n" "$(hostname)" "$(date --iso-8601=seconds)"'
ssh deployer@front01 'timeout 3 bash -c ": </dev/tcp/<app01の接続名>/<Appの待受port>"'
期待される出力
(2026-09-02 実測)
=== ルール ===
[ 1] 22/tcp                     ALLOW IN    172.16.10.22
[ 2] 8080/tcp                   ALLOW IN    172.16.10.23

=== 直後の管理接続 ===
management-ok host=learner01 time=2026-09-02T10:45:22+09:00

=== front01 から App service への probe ===
app01-probe ALLOW

6-4. front01 から app02 を許可する

app02 も同じ順序です。許可を 1 つ適用し、app02 への新しい SSH 接続を確認してから service probe を行います。

bash · コマンド
sudo ufw allow from <front01の接続元> to any port <Appの待受port> proto tcp
sudo ufw status numbered
bash · コマンド
ssh -o BatchMode=yes -o ConnectTimeout=5 deployer@<app02の接続名> \
  'printf "management-ok host=%s time=%s\n" "$(hostname)" "$(date --iso-8601=seconds)"'
ssh deployer@front01 'timeout 3 bash -c ": </dev/tcp/<app02の接続名>/<Appの待受port>"'
期待される出力
(2026-09-02 実測)
=== ルール ===
[ 1] 22/tcp                     ALLOW IN    172.16.10.22
[ 2] 8080/tcp                   ALLOW IN    172.16.10.23

=== 直後の管理接続 ===
management-ok host=learner02 time=2026-09-02T10:45:24+09:00

=== front01 から App service への probe ===
app02-probe ALLOW

6-5. app01 から db01 を許可する

db01 に、app01 から DB service への許可を 1 つだけ適用します。直後に db01 への新しい SSH 接続を確認し、app01 から DB service へ probe します。

bash · コマンド
sudo ufw allow from <app01の接続元> to any port <DBの待受port> proto tcp
sudo ufw status numbered
bash · コマンド
ssh -o BatchMode=yes -o ConnectTimeout=5 deployer@db01 \
  'printf "management-ok host=%s time=%s\n" "$(hostname)" "$(date --iso-8601=seconds)"'
ssh deployer@<app01の接続名> 'timeout 3 bash -c ": </dev/tcp/<db01の接続名>/<DBの待受port>"'
期待される出力
(2026-09-02 実測)
=== ルール ===
[ 1] 22/tcp                     ALLOW IN    172.16.10.22
[ 2] 5432/tcp                   ALLOW IN    172.16.10.20

=== 直後の管理接続 ===
management-ok host=db01 time=2026-09-02T01:45:24+00:00

=== app01 から DB service への probe ===
app01->db ALLOW

6-6. app02 から db01 を許可する

最後の必要経路も 1 つだけ適用します。db01 への新しい SSH 接続を確認してから、app02 から DB service へ probe します。

bash · コマンド
sudo ufw allow from <app02の接続元> to any port <DBの待受port> proto tcp
sudo ufw status numbered
bash · コマンド
ssh -o BatchMode=yes -o ConnectTimeout=5 deployer@db01 \
  'printf "management-ok host=%s time=%s\n" "$(hostname)" "$(date --iso-8601=seconds)"'
ssh deployer@<app02の接続名> 'timeout 3 bash -c ": </dev/tcp/<db01の接続名>/<DBの待受port>"'
期待される出力
(2026-09-02 実測)
=== ルール ===
[ 1] 22/tcp                     ALLOW IN    172.16.10.22
[ 2] 5432/tcp                   ALLOW IN    172.16.10.20
[ 3] 5432/tcp                   ALLOW IN    172.16.10.21

=== 直後の管理接続 ===
management-ok host=db01 time=2026-09-02T01:45:24+00:00

=== app02 から DB service への probe ===
app02->db ALLOW

ここまでで、絞った状態のまま入口経由の要求が通るようになります。

期待される出力
{"status":"ok","db":"ok","node":"app02"}
http=200

6-7. 必要経路以外が拒否されていることを確認する

必要な許可をすべて置いたら、それ以外を拒否するルールを書き足す作業はありません。ufw は有効化した時点で「許可していない受信は拒否」という既定で動いているため、書かなかったものが既に落ちているからです。ここで行うのは、その状態を自分の目で確かめることです。

🔴 「拒否を書いていないから塞がっていない」は、この方式では誤りです。 逆に明示的な拒否を書き足すと、公開面を知るために許可リストと拒否リストの両方を読む必要が生じ、読み手が増えるほど間違えやすくなります。許可だけを並べ、それ以外は既定で落とす形を保ちます。

各 VM で、有効な許可の全量を読みます。

bash · コマンド
sudo ufw status verbose
sudo ufw status numbered

表示された許可が STEP 1 の port 表と一致し、想定にない行が無いことを確認します。想定外の許可があれば番号を指定して 1 つずつ削除し、その都度、別ターミナルから新しい SSH 接続を確認します。

bash · コマンド
sudo ufw --force delete <想定にない許可の番号>

ssh -o BatchMode=yes -o ConnectTimeout=5 deployer@<対象VMの接続名> \
  'printf "management-ok host=%s time=%s\n" "$(hostname)" "$(date --iso-8601=seconds)"'
期待される出力
(2026-09-02 実測) 4 台とも `Default: deny (incoming), allow (outgoing)`。
--- front01 ---
[ 1] 22/tcp    ALLOW IN  172.16.10.22
[ 2] 8443/tcp  ALLOW IN  172.16.10.22
--- app01 ---
[ 1] 22/tcp    ALLOW IN  172.16.10.22
[ 2] 8080/tcp  ALLOW IN  172.16.10.23
--- app02 ---
[ 1] 22/tcp    ALLOW IN  172.16.10.22
[ 2] 8080/tcp  ALLOW IN  172.16.10.23
--- db01 ---
[ 1] 22/tcp    ALLOW IN  172.16.10.22
[ 2] 5432/tcp  ALLOW IN  172.16.10.20
[ 3] 5432/tcp  ALLOW IN  172.16.10.21

許可は合計 9 行だけです。port 表と突き合わせ、表に無い許可が 1 つも無いことを確認してください。

📔 コラム · この層は「出ていく通信」を止めていない ufw の既定は 受信は拒否・送信は許可です。つまりここまでの作業で絞ったのは、各 VM がどこから受け取るかだけです。各 VM から外側へ出ていく通信は、この層では止まっていません。 それでも演習環境から外部へは出られません。止めているのは受講者が触っているこの層ではなく、§03 の 3 で触れた別の層です。本章ではその層を変更せず、実装にも立ち入りません。 🔴 自分が書いたルールで塞がっているのか、別の層で塞がっているのかを取り違えないでください。 「届かない」という観測結果は同じでも、どちらが効いているかで、設定を変えたときの結果が変わります。

各回で新しい SSH 接続に失敗したら、その先のコマンドは実行しません。接続できなくなった場合は、演習セッションを終了すると環境が初期状態へ戻ります。

絞った後の到達性の表を作り、差を比較する

STEP 2 と 同じ 5 台、同じ 5 宛先、同じ順序、同じ probe_tcp 関数で測定します。コマンドや timeout を変えると、ルールの差と測定方法の差が混ざるため変更しません。

bash · コマンド
probe_tcp() {
  TARGET="$1"
  PORT="$2"
  LABEL="$3"
  if timeout 3 bash -c ": </dev/tcp/${TARGET}/${PORT}" 2>/dev/null; then
    printf '%s\t%s\tALLOW\n' "$(hostname)" "${LABEL}"
  else
    printf '%s\t%s\tDENY\n' "$(hostname)" "${LABEL}"
  fi
}

PHASE=after
{
  probe_tcp "<front01の接続名>" "<HTTPの待受port>" "front-http"
  probe_tcp "<front01の接続名>" "<HTTPSの待受port>" "front-https"
  probe_tcp "<app01の接続名>" "<Appの待受port>" "app01-service"
  probe_tcp "<app02の接続名>" "<Appの待受port>" "app02-service"
  probe_tcp "<db01の接続名>" "<DBの待受port>" "db-service"
} | tee "/tmp/ch5-reach-${PHASE}-$(hostname).tsv"

変更前と同じ手順で、5 台ぶんを 1 つの表にまとめます。

bash · コマンド
# workstation01 で 5 台ぶんを 1 つの表にまとめる
PHASE=after
OUT="/tmp/ch5-reachability-${PHASE}.tsv"
cat "/tmp/ch5-reach-${PHASE}-$(hostname).tsv" > "$OUT"
for H in front01 172.16.10.20 172.16.10.21 db01; do
  ssh deployer@"$H" "cat /tmp/ch5-reach-${PHASE}-\$(hostname).tsv" >> "$OUT"
done
sort -o "$OUT" "$OUT"
cat "$OUT"
期待される出力
(2026-09-02 実測)
db01	app01-service	DENY
db01	app02-service	DENY
db01	db-service	DENY
db01	front-http	DENY
db01	front-https	DENY
front01	app01-service	ALLOW
front01	app02-service	ALLOW
front01	db-service	DENY
front01	front-http	ALLOW
front01	front-https	ALLOW
learner01	app01-service	ALLOW
learner01	app02-service	DENY
learner01	db-service	ALLOW
learner01	front-http	DENY
learner01	front-https	DENY
learner02	app01-service	DENY
learner02	app02-service	ALLOW
learner02	db-service	ALLOW
learner02	front-http	DENY
learner02	front-https	DENY
workstation01	app01-service	DENY
workstation01	app02-service	DENY
workstation01	db-service	DENY
workstation01	front-http	DENY
workstation01	front-https	ALLOW

RESOLVE_DENY は 1 件もありません。名前解決は全台で生きたまま、TCP 到達性だけが変わったことを、 この列で示せます。

変更後の表を同じ列順で埋めます。

送信元front HTTPfront HTTPSapp01 serviceapp02 serviceDB service
workstation01DENYALLOWDENYDENYDENY
front01ALLOWALLOWALLOWALLOWDENY
app01DENYDENYALLOWDENYALLOW
app02DENYDENYDENYALLOWALLOW
db01DENYDENYDENYDENYDENY

STEP 2 と STEP 7 の記録を行単位で比較し、変化したセルだけを抽出します。

bash · コマンド
diff -u \
  /tmp/ch5-reachability-before.tsv \
  /tmp/ch5-reachability-after.tsv || true
期待される出力
(2026-09-02 実測) 変わったのは 25 セル中 14 セル。
db01	app01-service	ALLOW -> DENY
db01	app02-service	ALLOW -> DENY
db01	front-http	ALLOW -> DENY
front01	db-service	ALLOW -> DENY
front01	front-https	DENY -> ALLOW
learner01	app02-service	ALLOW -> DENY
learner01	front-http	ALLOW -> DENY
learner02	app01-service	ALLOW -> DENY
learner02	front-http	ALLOW -> DENY
workstation01	app01-service	ALLOW -> DENY
workstation01	app02-service	ALLOW -> DENY
workstation01	db-service	ALLOW -> DENY
workstation01	front-http	ALLOW -> DENY
workstation01	front-https	DENY -> ALLOW

変わったセルだけを読みます。

  1. 操作端末に残ったのは front-https だけ。App にも DB にも直接は届かなくなりました
  2. front01 から db-service が DENY に。入口は DB を必要としません
  3. App どうしが互いに DENY。同じ役割でも横に通す理由がありません
  4. db01 は全項目 DENY。DB は自分から接続しない役割であることが表に出ます

変わらなかったセルにも意味があります。learner01 から app01-service が ALLOW のままなのは 自分自身への接続だからで、ファイアウォールの対象ではありません。

次の 3 点を実測結果から確認します。

  1. workstation01 から front01 の HTTPS は成功し、App と DB への直接接続は失敗する。
  2. front01 から両 App の service は成功する。
  3. 両 App から DB service は成功し、それ以外の不要な直接経路は失敗する。

最後に HTTPS の入口から教材アプリのタップ要求を送り、入口を経由した処理が成功することを確認します。

bash · コマンド
EVENT_ID="event-tls-path-check"
curl --fail --show-error --include \
  --cacert "$HOME/ch5-tls/lab-ca.crt" \
  -H "X-Event-ID: ${EVENT_ID}" \
  -H 'Content-Type: application/json' \
  --data "{\"card_id\":\"card-demo\",\"event_id\":\"${EVENT_ID}\",\"amount\":10}" \
  "https://<front01のTLS接続名>:<HTTPSの待受port>/tap"
期待される出力
(2026-09-02 実測)
=== 応答 ===
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{"ok":true,"card_id":"card-demo","balance":990,"event_id":"event-tls-path-check","node":"app01"}

=== 入口の記録 ===
172.16.10.22 POST /tap HTTP/1.1 status=200 event_id=event-tls-path-check upstream=172.16.10.20:8080 upstream_status=200

=== App の記録(app01)===
{"event_id":"event-tls-path-check","card_id":"card-demo","amount":10,"node":"app01","status":200,"result":"ok"}

=== DB の記録 ===
       event_id       |  card_id  | amount |          created_at
----------------------+-----------+--------+-------------------------------
 event-tls-path-check | card-demo |     10 | 2026-09-02 01:48:31.237054+00
(1 row)

  card_id  | balance
-----------+---------
 card-demo |     990

同じ event_id で入口・App・DB の 3 つの記録が並びます。残高は 1000 から 990 へ 1 回だけ減っています。

📔 DB 側に node 列はありません。どの App が処理したかは App の記録にだけ出ます。
「どこで処理されたか」と「何が確定したか」は別の場所に残る、という形をここで確認してください。

ゴール到達 ✓ ① server certificate の SAN・発行者・期限を読んだ / ② 信頼前の失敗と信頼後の成功を比較した / ③ 5 台の変更前後の到達表を同じコマンド群で作った / ④ 表の差から、入口 HTTPS、入口から App、App から DB だけが必要なサービス経路として残ったことを証明した。

TLS と到達性を 1 要因ずつ壊して戻す

この STEP は 2 回の独立した実験です。最初の TLS の失敗を直し、正常へ戻ったことを確認してから、到達性の失敗へ進みます。2 要因を同時に壊しません。

8-1. 名前が一致しない certificate で TLS の失敗を見る

演習用 CA を使い、接続に使う名前を SAN に含めない別の server certificate を作ります。秘密鍵は STEP 3 で作ったものをそのまま使い、名前だけを変えます。 秘密鍵を作り直すと、失敗の原因が「名前が違うから」なのか「鍵の組が違うから」なのか区別できなくなります。差し替えるのは certificate 1 本だけです。

bash · コマンド
# front01 で、同じ秘密鍵から SAN の違う CSR を作る
ssh deployer@front01
sudo openssl req -new -sha256 \
  -key /etc/nginx/tls/front01.key \
  -subj "/CN=<接続に使わない名前>" \
  -out /tmp/wrong-name.csr
bash · コマンド
# workstation01 で CSR を受け取り、演習用 CA で署名する
scp deployer@<front01の接続名>:/tmp/wrong-name.csr "$HOME/ch5-tls/wrong-name.csr"
cd "$HOME/ch5-tls"

cat > wrong-name.ext <<'EOF'
subjectAltName=DNS:<接続に使わない名前>
keyUsage=digitalSignature,keyEncipherment
extendedKeyUsage=serverAuth
EOF

openssl x509 -req -sha256 \
  -in wrong-name.csr \
  -CA lab-ca.crt \
  -CAkey lab-ca.key \
  -CAcreateserial \
  -days 7 \
  -extfile wrong-name.ext \
  -out wrong-name.crt

scp wrong-name.crt deployer@<front01の接続名>:/tmp/wrong-name.crt

front01 で正常な certificate を退避し、名前不一致の certificate へ差し替えて構文検査後に reload します。秘密鍵は触りません。

bash · コマンド
sudo cp /etc/nginx/tls/front01.crt /tmp/ch5-front01.crt.good
sudo install -m 644 /tmp/wrong-name.crt /etc/nginx/tls/front01.crt
sudo nginx -t
sudo systemctl reload nginx

workstation01 から、正しい接続名のまま接続して失敗を保存します。

bash · コマンド
curl --show-error --verbose \
  --cacert "$HOME/ch5-tls/lab-ca.crt" \
  "https://<front01のTLS接続名>:<HTTPSの待受port>/health"
printf 'name_mismatch_exit=%s\n' "$?"
期待される出力
(2026-09-02 実測)
=== 差し替えた certificate ===
subject=CN = not-front01
X509v3 Subject Alternative Name:
    DNS:not-front01

=== 正しい接続名のまま接続 ===
*  subject: CN=not-front01
*  subjectAltName does not match front01
*  SSL: no alternative certificate subject name matches target host name 'front01'
curl: (60) SSL: no alternative certificate subject name matches target host name 'front01'
name_mismatch_exit=60

Verification error: hostname mismatch
Verify return code: 62 (hostname mismatch)

失敗の理由が STEP 5 とは違うことに注目してください。STEP 5 は「発行者を信頼できない」(unable to verify the first certificate)、ここは「名前が一致しない」(hostname mismatch)です。 どちらも TLS の検証失敗ですが、直すべき場所が違います。

⚠️ reload した直後に確認すると、壊す前の結果が返ることがあります。 nginx の reload は動いている worker(nginx の要求処理プロセス)をすぐには終わらせないため、古い certificate のまま 応答し続ける時間があります。差し替えたことを確かめてから測ってください。

bash · コマンド
# 提示されている certificate を先に見る
openssl s_client -connect "<front01の接続名>:<HTTPSの待受port>" -servername "<front01のTLS接続名>" </dev/null \
  | openssl x509 -noout -subject

正常な certificate を戻し、構文検査、reload、同じ curl の成功まで確認します。戻すのも certificate 1 本だけです。壊したものと戻すものが同じ 1 本であることを確かめてください。

bash · コマンド
sudo cp /tmp/ch5-front01.crt.good /etc/nginx/tls/front01.crt
sudo chmod 644 /etc/nginx/tls/front01.crt
sudo nginx -t
sudo systemctl reload nginx
bash · コマンド
curl --fail --show-error \
  --cacert "$HOME/ch5-tls/lab-ca.crt" \
  "https://<front01のTLS接続名>:<HTTPSの待受port>/health"
期待される出力
(2026-09-02 実測)
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

{"status":"ok","db":"ok","node":"app01"}
http=200

Verify return code: 0 (ok)

戻したのも certificate 1 本だけです。秘密鍵にも nginx の設定にも触れていません。

8-2. 必要な許可を 1 つ外して到達性の失敗を見る

TLS が正常へ戻ったことを確認してから、front01 から app01 への service 許可だけを一時的に外します。管理接続の許可と、ほかのサービス経路は変更しません。

外すのは app01 に登録した「front01 からの App service 許可」1 件です。番号を指定して削除するため、先に番号を確認してから削除します。

bash · コマンド
# app01 で、外す 1 件の番号を確認してから削除する
sudo ufw status numbered
sudo ufw --force delete <front01からのApp service許可の番号>
sudo ufw status verbose

🔴 番号は削除のたびに振り直されます。 続けて 2 件消すつもりで番号を控えておくと、2 件目で別のルールを消します。1 件消したら必ず番号を取り直してください。

ルール変更の直後に、まず app01 への新しい SSH 接続が生きていることを確認します。その後、front01 から app01 への service probe が失敗することを確認します。

bash · コマンド
ssh -o BatchMode=yes -o ConnectTimeout=5 deployer@<app01の接続名> \
  'printf "management-ok host=%s time=%s\n" "$(hostname)" "$(date --iso-8601=seconds)"'
ssh deployer@front01 \
  'timeout 3 bash -c ": </dev/tcp/<app01の接続名>/<Appの待受port>"'
printf 'service_probe_exit=%s\n' "$?"
期待される出力
(2026-09-02 実測)
=== 削除前後の番号 ===
[ 1] 22/tcp    ALLOW IN  172.16.10.22
[ 2] 8080/tcp  ALLOW IN  172.16.10.23     ← これを削除
(削除後)
[ 1] 22/tcp    ALLOW IN  172.16.10.22

=== 直後の管理接続(生きている)===
management-ok host=learner01 time=2026-09-02T10:50:20+09:00

=== front01 からの service probe ===
app01-probe DENY
app02-probe ALLOW     ← 変えていない方は通る

=== 入口経由を 3 回 ===
{"status":"ok","db":"ok","node":"app02"}  http=200
curl: (28) Operation timed out after 10002 milliseconds with 0 bytes received
http=000
{"status":"ok","db":"ok","node":"app02"}  http=200

🔴 入口経由の要求は「毎回失敗」しません。入口は 2 台へ順番に振り分けるので、 app01 に当たった回だけ固まります。必ず複数回試してください。1 回だけ試して成功すると 「壊れていない」と誤解します。

どちらに振られたかは、入口ログの upstream= で分かります。 「たまに固まる」は現場でいちばん診断しにくい壊れ方で、その形をここで体験できます。

失敗を保存したら、外した許可 1 件を戻します。直後に管理接続、service probe、HTTPS 入口経由の要求を順に確認します。

bash · コマンド
# app01 で、外したのと同じ 1 件を戻す
sudo ufw allow from <front01の接続元> to any port <Appの待受port> proto tcp
sudo ufw status verbose
bash · コマンド
ssh -o BatchMode=yes -o ConnectTimeout=5 deployer@<app01の接続名> \
  'printf "management-ok host=%s time=%s\n" "$(hostname)" "$(date --iso-8601=seconds)"'
ssh deployer@front01 'timeout 3 bash -c ": </dev/tcp/<app01の接続名>/<Appの待受port>"'
curl --fail --show-error \
  --cacert "$HOME/ch5-tls/lab-ca.crt" \
  "https://<front01のTLS接続名>:<HTTPSの待受port>/health"
期待される出力
(2026-09-02 実測)
=== 戻したルール ===
[ 1] 22/tcp    ALLOW IN  172.16.10.22
[ 2] 8080/tcp  ALLOW IN  172.16.10.23

=== 直後の管理接続 ===
management-ok host=learner01 time=2026-09-02T10:50:57+09:00

=== front01 からの service probe ===
app01-probe ALLOW

=== 入口経由を 6 回(両方へ戻ったことを示す)===
{"status":"ok","db":"ok","node":"app01"}  http=200
{"status":"ok","db":"ok","node":"app02"}  http=200
{"status":"ok","db":"ok","node":"app01"}  http=200
{"status":"ok","db":"ok","node":"app02"}  http=200
{"status":"ok","db":"ok","node":"app01"}  http=200
{"status":"ok","db":"ok","node":"app02"}  http=200

外したのと同じ 1 件を戻しただけで、両方の App へ振り分けが戻りました。 1 回では確認になりません(たまたま app02 に当たるため)。複数回試して両方の node が出ることが 戻った証拠です。

TLS の実験ではクライアント側の証明書検証エラー、到達性の実験では TCP 接続の失敗が観測されます。表示の出どころが異なることを確認し、両方が正常へ戻ってから章を終えてください。

演習の証跡を保存した後、章が作ったものを台ごとに削除します。これは前章の再学習ではなく、自分が作ったものは毎章自分で消すという運用上の規律です。削除コマンドの成功ではなく、最後の 8 分類の検査を修了条件にします。

8-3. 記録して App を止める

4 台で hostname、package 基準、UFW の状態とルールを記録します。db01 では PostgreSQL が動いているうちに設定パスを取得します。この章の UFW 追加は、front01 の管理・HTTPS、各 App の管理・front01 からの service、db01 の管理・2 App からの DB service の計 9 件です。

bash · 記録
hostname
test -s /tmp/part4-package-before.tsv
sudo ufw status verbose
sudo ufw status numbered
# 以下 3 行は db01 だけ
PG_CONF="$(sudo -u postgres psql -Atc 'SHOW config_file;')"
PG_HBA="$(sudo -u postgres psql -Atc 'SHOW hba_file;')"
PG_CONF_DIR="$(dirname "$PG_CONF")"

app01 で次を実行し、終了後に新しく app02 へ接続してもう一度実行します。先頭の出力を残し、同じ台で 2 回実行していないことを確認します。

bash · app01 / app02
hostname
sudo systemctl disable --now tapapi.service
systemctl is-active tapapi.service || true

8-4. front01 の nginx 参照を外す

TLS 資産がまだ存在する状態で、有効化先、章専用設定本体の順に外します。nginx 本体がある間に nginx -t と reload を通し、章専用 access log も削除します。

bash · front01
hostname
sudo rm -f "<nginxの有効化先>"
sudo rm -f "<nginxの章専用設定ファイル>"
sudo nginx -t
sudo systemctl reload nginx
sudo rm -f /var/log/nginx/tap_access.log /var/log/nginx/tap_access.log.*

8-5. db01 の DB、role、設定、cluster を戻す

DB、role(DB 内でユーザや権限を表すもの)の順に削除し、動的に取得した設定パスから章専用 drop-in と pg_hba.conf の 2 行を除去します。再起動後に DB・role・追記が 0 行であることを確認してから、章が作った cluster(1 つのデータディレクトリで管理される DB のまとまり)と package を削除します。

bash · db01
(
sudo -u postgres dropdb --if-exists transit_lab
sudo -u postgres dropuser --if-exists tap_app
sudo rm -f "$PG_CONF_DIR/conf.d/part4.conf"
sudo sed -i -e '\|^host transit_lab tap_app 172\.16\.10\.20/32 scram-sha-256$|d' -e '\|^host transit_lab tap_app 172\.16\.10\.21/32 scram-sha-256$|d' "$PG_HBA"
sudo systemctl restart postgresql
test "$(sudo grep -cE '^host transit_lab tap_app (172\.16\.10\.20|172\.16\.10\.21)/32 scram-sha-256$' "$PG_HBA" || true)" -eq 0
test "$(sudo -u postgres psql -Atc "SELECT count(*) FROM pg_database WHERE datname='transit_lab';")" -eq 0
test "$(sudo -u postgres psql -Atc "SELECT count(*) FROM pg_roles WHERE rolname='tap_app';")" -eq 0
# 基準状態の db01 に PostgreSQL は無い。ここに出る cluster は本章が作ったもの。
# 見覚えのない cluster が出たら、消さずに止めて演習環境の担当へ知らせる。
pg_lsclusters --no-header
pg_lsclusters --no-header | awk '{print $1, $2}' | while read -r version cluster; do sudo pg_dropcluster --stop "$version" "$cluster"; done

mapfile -t chapter_packages < <(comm -13 <(cut -f1 /tmp/part4-package-before.tsv | LC_ALL=C sort) <(dpkg-query -W -f='${binary:Package}\n' | LC_ALL=C sort))
unexpected=()
for pkg in "${chapter_packages[@]}"; do
  case "${pkg%:amd64}" in
    postgresql|postgresql-16|postgresql-client|postgresql-client-16|postgresql-client-common|postgresql-common|libcommon-sense-perl|libjson-perl|libjson-xs-perl|libllvm17t64|libpq5|libtypes-serialiser-perl|ssl-cert) ;;
    *) unexpected+=("$pkg") ;;
  esac
done
((${#unexpected[@]} == 0)) || { printf '想定外の package: %s\n' "${unexpected[@]}" >&2; exit 1; }

APT_ONLY_PART4=(
  -o Dir::Etc::sourcelist=/dev/null
  -o Dir::Etc::sourceparts=/etc/apt/sources.list.d.part4
  -o APT::Get::List-Cleanup=0
)
((${#chapter_packages[@]} == 0)) || sudo DEBIAN_FRONTEND=noninteractive apt-get "${APT_ONLY_PART4[@]}" purge -y "${chapter_packages[@]}"

sudo userdel postgres 2>/dev/null || true
sudo groupdel postgres 2>/dev/null || true
sudo groupdel ssl-cert 2>/dev/null || true
sudo chown root:root /etc/ssl/private
sudo chmod 0700 /etc/ssl/private
getent passwd postgres || true
getent group postgres || true
getent group ssl-cert || true
stat -c '%a %u %g %n' /etc/ssl/private
)
rc=$?
((rc == 0)) || echo 'SSH 接続は維持したまま処理を中止しました' >&2

8-6. UFW を inactive・0 ルールへ戻す

workstation01 に制御用ターミナルを残し、4 台を 1 台ずつ処理します。先に無効化して管理接続を保ち、その後で章が作った永続ルールを reset します。

bash · 4 台で 1 台ずつ
hostname
sudo ufw --force disable
# ufw reset は元の規則を *.rules.<日時> へ「名前を変えて移す」ので、更新日時は元のまま。
# 日時では選べないため、reset の前に在った退避の一覧を控え、増えた分だけを消す
mapfile -t UFW_OLD < <(ls /etc/ufw/*.rules.[0-9]*_[0-9]* 2>/dev/null)
sudo ufw --force reset
for f in /etc/ufw/*.rules.[0-9]*_[0-9]*; do
  [ -e "$f" ] || continue
  printf '%s\n' "${UFW_OLD[@]}" | grep -qxF -- "$f" || sudo rm -f -- "$f"
done
printf 'ufw の退避: 開始前 %s 件 / 現在 %s 件(同数なら今回の分は消えた)\n' "${#UFW_OLD[@]}" "$(ls /etc/ufw/*.rules.[0-9]*_[0-9]* 2>/dev/null | wc -l)"
sudo ufw status verbose
sudo ufw status numbered

8-7. App、nginx、TLS 資産を削除する

app01 と app02 では App 用、front01 では nginx 用のブロックを、それぞれ対象ホストへ接続して実行します。役割の異なる package 許可リストを同じホストで続けて評価しないでください。

bash · app01 / app02 で 1 台ずつ
(
# app01 / app02
hostname
sudo rm -f /etc/systemd/system/tapapi.service
sudo systemctl daemon-reload
sudo rm -rf /opt/tapapi /etc/tapapi
sudo userdel tapapi 2>/dev/null || true
getent group tapapi >/dev/null && sudo groupdel tapapi || true

mapfile -t chapter_packages < <(comm -13 <(cut -f1 /tmp/part4-package-before.tsv | LC_ALL=C sort) <(dpkg-query -W -f='${binary:Package}\n' | LC_ALL=C sort))
unexpected=()
for pkg in "${chapter_packages[@]}"; do
  case "${pkg%:amd64}" in
    python3-psycopg2|libpq5) ;;
    *) unexpected+=("$pkg") ;;
  esac
done
((${#unexpected[@]} == 0)) || { printf '想定外の package: %s\n' "${unexpected[@]}" >&2; exit 1; }
APT_ONLY_PART4=(
  -o Dir::Etc::sourcelist=/dev/null
  -o Dir::Etc::sourceparts=/etc/apt/sources.list.d.part4
  -o APT::Get::List-Cleanup=0
)
((${#chapter_packages[@]} == 0)) || sudo DEBIAN_FRONTEND=noninteractive apt-get "${APT_ONLY_PART4[@]}" purge -y "${chapter_packages[@]}"

)
rc=$?
((rc == 0)) || echo 'SSH 接続は維持したまま処理を中止しました' >&2

front01 では、nginx が TLS 資産を参照しなくなったことを確認してから次を実行します。

bash · front01
(
# front01(nginx が参照しなくなった後)
APT_ONLY_PART4=(
  -o Dir::Etc::sourcelist=/dev/null
  -o Dir::Etc::sourceparts=/etc/apt/sources.list.d.part4
  -o APT::Get::List-Cleanup=0
)
mapfile -t chapter_packages < <(comm -13 <(cut -f1 /tmp/part4-package-before.tsv | LC_ALL=C sort) <(dpkg-query -W -f='${binary:Package}\n' | LC_ALL=C sort))
unexpected=()
for pkg in "${chapter_packages[@]}"; do
  case "${pkg%:amd64}" in
    nginx|nginx-common) ;;
    *) unexpected+=("$pkg") ;;
  esac
done
((${#unexpected[@]} == 0)) || { printf '想定外の package: %s\n' "${unexpected[@]}" >&2; exit 1; }
((${#chapter_packages[@]} == 0)) || sudo DEBIAN_FRONTEND=noninteractive apt-get "${APT_ONLY_PART4[@]}" purge -y "${chapter_packages[@]}"

sudo rm -rf /etc/nginx/tls
sudo rm -f /tmp/front01.csr /tmp/front01.crt /tmp/wrong-name.csr /tmp/wrong-name.crt /tmp/ch5-front01.crt.good
)
rc=$?
((rc == 0)) || echo 'SSH 接続は維持したまま処理を中止しました' >&2

8-8. APT source、一時物、workstation01 の TLS 資産を戻す

4 台で章専用ディレクトリ /etc/apt/sources.list.d.part4/ と keyring を削除します。既存 source は最初から変更していないため、復元も無限定の apt update も不要です。

bash · 4 台共通
sudo rm -rf /etc/apt/sources.list.d.part4
sudo rm -f /etc/apt/keyrings/infra-study-archive-keyring.asc
rm -f /tmp/infra-study-archive-keyring.asc /tmp/tapapi-1.1.tar.gz /tmp/ch5-reach-before-*.tsv /tmp/ch5-reach-after-*.tsv
test ! -e /etc/apt/sources.list.d.part4
test ! -e /etc/apt/keyrings/infra-study-archive-keyring.asc
test ! -e /tmp/infra-study-archive-keyring.asc
[ -z "${TAPAPI_TMP:-}" ] || rm -rf -- "$TAPAPI_TMP"

本章の gpg 呼び出しはすべて mktemp -d の使い捨て GnuPG home で行い、使用後にその場で削除しています(C-1)。そのため章のどの操作も ~/.gnupg に触れず、削除対象がありません。workstation01 の一時物だけを削除します。

bash · workstation01 の一時物
# workstation01
rm -f /tmp/ch5-reachability-before.tsv /tmp/ch5-reachability-after.tsv
rm -rf "$HOME/ch5-tls"

8-9. 8 分類で残留を検査する

各台で package 一覧を章開始前と比較します。差分があれば、直接指定した package だけ消して完了とは判定しません。apt -s autoremove --purge の候補を差分と照合し、章由来だけを削除して差分 0 行まで再確認します。

bash · 検査
dpkg-query -W -f='${binary:Package}\t${Version}\n' | sort > /tmp/part4-package-after.tsv
diff -u /tmp/part4-package-before.tsv /tmp/part4-package-after.tsv
systemctl status tapapi.service --no-pager || true
systemctl status nginx --no-pager || true
systemctl status postgresql --no-pager || true
sudo ss -lntp | grep -E ':(5432|8080|8443)([[:space:]]|$)' || true
test ! -e /opt/tapapi; test ! -e /etc/tapapi; test ! -e /etc/systemd/system/tapapi.service
test ! -e "<nginxの章専用設定ファイル>"; test ! -e "<nginxの有効化先>"
test ! -e /etc/apt/sources.list.d.part4; test ! -e /etc/apt/keyrings/infra-study-archive-keyring.asc
getent passwd tapapi || true; getent group tapapi || true
getent passwd postgres || true; getent group postgres || true; getent group ssl-cert || true
stat -c '%a %u %g %n' /etc/ssl/private 2>/dev/null || true
sudo ufw status numbered
test "$(ls /etc/ufw/*.rules.20* 2>/dev/null | wc -l)" -eq 0
test ! -e /etc/nginx/tls/front01.key; test ! -e /etc/nginx/tls/front01.crt
test ! -e "$HOME/ch5-tls"

8-10. 8 分類の外を manifest 差分で確認する

8 分類を自分で列挙して検査したあと、その外側に残る変更を、構築前後の sorted manifest で確認します。差分は列挙の代わりではなく、列挙から漏れた分類を見つける安全網です。全差分は /var/tmp/part4-chapter-manifest.diff に一時保存し、「残留候補」と「更新日時だけ変わった通常ファイル」に分けて全件を画面に表示してから削除します。途中で切りません。表示に失敗したときは、構築前・章末の manifest と差分ファイルを消さずに残し、理由を表示します。表示された理由を確かめるまで、後片付けへ進まないでください。

次の除外には、それぞれ理由があります。

  • /var/lib/apt/ は apt が索引を更新する場所で、章の作業とは無関係に必ず変わります。
  • /var/lib/dpkg/ は package 操作で更新される管理情報で、package 差分として別に判定します。
  • /var/lib/systemd/ は service の状態管理で更新される場所で、service・unit の分類として別に判定します。
  • /var/log/ は操作の証跡そのもので、コマンドを実行するだけでも変わります。
  • /var/cache/ は再生成できる一時的な cache で、章の構築物とは分けて扱います。
  • .bash_history は操作履歴として更新されるため、章が配置したファイルとは分けて扱います。
  • /var/lib/snapd/state.json・/var/lib/snapd/maintenance.json と /var/lib/logrotate/status は、snapd と logrotate が章と無関係に定期的に書き換える状態ファイルです。親ディレクトリは除外しません。
bash · manifest 差分
sudo find /etc /var/lib /srv /opt /usr/local "$HOME" \
  -not -path '/var/lib/apt/*' \
  -not -path '/var/lib/dpkg/*' \
  -not -path '/var/lib/systemd/*' \
  -not -path '/var/log/*' \
  -not -path '/var/cache/*' \
  -not -name '.bash_history' \
  -not -path '/etc/passwd-' -not -path '/etc/group-' \
  -not -path '/etc/shadow-' -not -path '/etc/gshadow-' \
  -not -path '/var/lib/fwupd' -not -path '/var/lib/fwupd/*' \
  -not -path '/etc/fwupd' -not -path '/etc/fwupd/*' \
  -not -path '/var/lib/ucf' -not -path '/var/lib/ucf/*' \
  -not -path '/var/lib/landscape' -not -path '/var/lib/landscape/*' \
  -not -path '/etc/ld.so.cache' \
  -not -path '/var/lib/ubuntu-release-upgrader/release-upgrade-available' \
  -not -path '/var/lib/snapd/state.json' \
  -not -path '/var/lib/snapd/maintenance.json' \
  -not -path '/var/lib/logrotate/status' \
  -printf '%p\t%y\t%m\t%U\t%G\t%s\t%T@\n' 2>/dev/null \
  | LC_ALL=C sort \
  | sudo tee /tmp/part4-manifest-after.tsv >/dev/null
sudo diff -u /tmp/part4-manifest-before.tsv /tmp/part4-manifest-after.tsv > /var/tmp/part4-chapter-manifest.diff
diff_rc=$?
if [ "$diff_rc" -le 1 ]; then
  if ! awk 'BEGIN { FS = "\t"; c = "LC_ALL=C sort" }
      /^\+\+\+|^---|^@@/ { next }
      /^[+-]/ {
        if (NF != 7) { printf "manifest の列数が 7 ではありません: %s\n", $0 > "/dev/stderr"; bad = 1; exit 2 }
        t++; s = substr($1, 1, 1); p = substr($1, 2)
        k = $2 " " $3 " " $4 " " $5 " " ($2 == "d" ? "-" : $6)
        if (s == "-") { if (p in b) { dup = p; exit 2 } b[p] = k; bm[p] = $7; bt[p] = $2 }
        else          { if (p in a) { dup = p; exit 2 } a[p] = k; am[p] = $7 }
      }
      END {
        if (bad) exit 2
        if (dup != "") { printf "同じパスが重複しています: %s\n", dup > "/dev/stderr"; exit 2 }
        print "== 残留候補(0 件で合格)=="
        n = 0
        for (p in b) {
          if (!(p in a)) { print "消えた   " p | c; n++ }
          else if (a[p] != b[p]) { print "変わった " p "  (" b[p] " -> " a[p] ")" | c; n++ }
        }
        for (p in a) if (!(p in b)) { print "増えた   " p | c; n++ }
        fflush(); close(c)
        print "残留候補: " n " 件"
        print "== 更新日時だけ変わった通常ファイル(1 件ずつ理由を説明できること)=="
        m = 0; d = 0
        for (p in b) if ((p in a) && a[p] == b[p] && am[p] != bm[p]) { if (bt[p] == "d") d++; else { print p | c; m++ } }
        fflush(); close(c)
        print "更新日時だけ変わった通常ファイル: " m " 件 / ディレクトリ: " d " 件"
        print "差分行総数: " (t + 0)
      }' /var/tmp/part4-chapter-manifest.diff; then
    echo "差分の表示に失敗しました。構築前・章末の manifest と /var/tmp/part4-chapter-manifest.diff は残してあります。表示された理由を確認してください" >&2
  fi
else
  echo "差分を採れませんでした(diff の終了コード $diff_rc)。構築前 manifest /tmp/part4-manifest-before.tsv があるか確認してください" >&2
fi

表示は 2 つの節に分かれます。合格の条件は、1 つ目の「残留候補」が 0 件であることです。残留候補は、構築前になかったパスが増えた、あったパスが消えた、種別・権限・所有者・サイズが変わった、のいずれかです(ディレクトリのサイズは比べません)。

2 つ目の「更新日時だけ変わった通常ファイル」は、中身の属性は同じで更新日時だけが違うファイルです。ここに出る行は 1 件ずつ、次のどちらかで説明できなければ完了にしません。

  • OS の自動処理(package 索引の更新、通知用の記録など)が書いたもの。
  • 章の操作の跡で、中身は戻っているもの(例: ユーザを作って消したあとの /etc/passwd)。

どちらでも説明できない行が 1 件でもあれば、それは自分が挙げた 8 分類の外側にある残留です。何の分類にあたるかを考えて「9 番目の分類」を自分の後片付け手順へ足し、解消後に再確認してください。

期待される出力
(形の例です。M・K・T には実行したときの件数が入ります。更新日時だけ変わった通常ファイルの行と件数は、台・章によって変わります。合格の条件は「残留候補: 0 件」です)
== 残留候補(0 件で合格)==
残留候補: 0 件
== 更新日時だけ変わった通常ファイル(1 件ずつ理由を説明できること)==
(該当するパスが 1 行ずつ並びます)
更新日時だけ変わった通常ファイル: M 件 / ディレクトリ: K 件
差分行総数: T

合格条件は、package 差分 0 行、章専用 service なし、章の port 待受なし、DB と role 0 行(8-5 で確認)、章専用設定なし、tapapi user・group なし、UFW が 4 台とも inactive・0 ルール・退避ファイル 0 個、TLS 秘密鍵と certificate なし、ファイル差分に未解消の残留候補なしです。改訂後の実機出力は採り直してから掲載します。

検査結果を証跡へ保存した後、検査用の一時一覧も削除します。

bash · 最後の一時物
rm -f /tmp/part4-package-before.tsv /tmp/part4-package-after.tsv
sudo rm -f /tmp/part4-manifest-before.tsv /tmp/part4-manifest-after.tsv /var/tmp/part4-chapter-manifest.diff
test ! -e /var/tmp/part4-chapter-manifest.diff

つまずきポイント

ハマり #1 · certificate が信頼されない

HTTPS の接続は始まりますが、発行者を信頼できないという検証失敗になります。

確認順序

  1. curl が演習用 CA を --cacert で指定しているか確認する。
  2. openssl x509 -issuer -subject で server certificate の発行者を読む。
  3. openssl verify -CAfile で作成物の chain を確認する。
  4. openssl s_client -showcerts で入口が実際に提示した certificate を読む。

ハマり #2 · certificate の名前が一致しない

演習用 CA は信頼されていますが、接続名の検証に失敗します。

確認順序

  1. curl の URL に使った名前を記録する。
  2. openssl x509 -ext subjectAltName で SAN を読む。
  3. openssl s_client -servername に渡した名前を確認する。
  4. nginx が提示している certificate を確認する。

ハマり #3 · ルール適用後に自分の接続が切れた

現在の SSH が応答しない、または別ターミナルから新しい SSH 接続を作れません。

確認順序

  1. その先のルール適用を直ちに止める。
  2. 別ターミナルから対象 VM へ新しい SSH 接続を 1 回だけ試す。
  3. 接続できなければ、同じ経路から修正できると思い込んで操作を重ねない。

ハマり #4 · 入口経由は通るのに直接接続も通ってしまう

HTTPS の入口経由の要求は成功しますが、workstation01 から App または DB への直接 probe も成功します。

確認順序

  1. STEP 7 の表で、どの送信元・宛先のセルが想定外かを 1 つに絞る。
  2. 対象 VM の待受 port と、probe した port が一致するか確認する。
  3. 対象 VM に反映された service network 側のルールを読み、送信元と順序を確認する。
  4. STEP 2 の同じセルと比較し、変更前後で結果が本当に同じか確認する。

修了確認

問 1 · TLS の 3 つの確認点を説明する

server certificate を検証するとき、名前・発行者・期限はそれぞれ何を確認しますか。

問 2 · 変更前の到達表が必要な理由を説明する

変更後の表だけでは、サービス経路を絞れた証明として不足するのはなぜですか。

問 3 · 1 ルールごとに管理接続を確認する

複数のルールをまとめて適用し、最後に SSH を確認する手順が危険なのはなぜですか。

問 4 · TLS の失敗と到達性の失敗を切り分ける

HTTPS 要求が失敗したとき、certificate の名前不一致と TCP 到達不可を、どの観測結果から区別しますか。

段階ヒント(問 1〜問 4)

問 1 のヒントと答え

ヒント 1 段 · certificate の欄を見る

接続時に指定したもの、chain の上位、現在時刻と比べる欄を探してください。

ヒント 2 段 · 確認コマンド

openssl x509 -subject -issuer -dates -ext subjectAltName の出力を、接続名と現在時刻に照らします。

答えを見る

名前は接続名が SAN に含まれるか、発行者は信頼する CA へ chain をたどれるか、期限は現在時刻が有効期間内かを確認します。3 点は別々の条件です。

問 2 のヒントと答え

ヒント 1 段 · 変化を証明するには

変更後に DENY だったセルが、変更前にどうだったかを考えてください。

ヒント 2 段 · 同じ測定を使う

STEP 2 と STEP 7 は、同じ 5 台、同じ 5 宛先、同じ順序、同じ probe を使います。

答えを見る

変更後だけでは、ルールで拒否されたのか、もともと到達できなかったのかを区別できません。変更前後を同じ方法で測り、変化したセルを比較することで、絞り込みの効果を示せます。

問 3 のヒントと答え

ヒント 1 段 · 失敗した位置を考える

5 個まとめて適用した後に接続が切れたら、どの変更が原因か分かるでしょうか。

ヒント 2 段 · 新しい接続を作る

現在の SSH が残っていることだけでなく、別ターミナルから対象 VM へ新しい SSH 接続を作れるかを毎回確認します。

答えを見る

途中で管理接続を遮断すると、以後のルールを直せず、どの変更で切れたかも特定しにくくなります。管理接続を先に明示的に許可し、1 ルール適用するごとに新しい SSH 接続を確認してから次へ進みます。

問 4 のヒントと答え

ヒント 1 段 · 失敗した層を見る

TCP 接続が成立する前の失敗と、TLS handshake 中の検証失敗を分けてください。

ヒント 2 段 · 観測場所

TCP probe の終了 status、curl --verbose の検証エラー、openssl s_client が提示する SAN と chain を比較します。

答えを見る

名前不一致では TCP 接続と TLS handshake は進み、クライアントが SAN と接続名の不一致を報告します。到達不可では TLS 検証へ進む前に TCP probe が失敗します。エラー文だけでなく、どの段階まで進んだかで区別します。

部品をそろえ、自分で建てて引き渡す

本章で、入口の信頼と、5 台をまたぐ最小のサービス経路を観測できる形になりました。次章では、これまで用意した部品を自分で建て直し、受入条件を検査して、運用へ引き渡せる状態かを判断します。

Chapter 6: 改札サービスを建て直し、運用へ引き渡す 🎓

  • 入口、App、DB を自分で組み立てる
  • 正常系と故障系を受入条件で検査する
  • 証跡をそろえ、運用へ引き渡せるか判断する

参考資料