Add platform infrastructure configuration

This commit is contained in:
donghyeon-ka
2026-08-28 17:35:41 +09:00
parent fa76531e5b
commit 16c337bcc9
302 changed files with 83259 additions and 1 deletions
+123
View File
@@ -0,0 +1,123 @@
#!/usr/bin/env bash
set -Eeuo pipefail
set +x
umask 077
readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
# shellcheck source=common.sh
source "${SCRIPT_DIR}/common.sh"
usage() {
cat <<'USAGE'
Usage: bash infrastructure/networking/traefik/scripts/apply-observe.sh --execute
Applies JSON access logging to the k3s-managed Traefik HelmChartConfig.
It does not trust any forwarded header. The existing loopback-only
NodePort 30080/30443 boundary is declared explicitly so Helm reconciliation
cannot replace it with chart defaults.
USAGE
}
[[ "${1:-}" == "--execute" && "$#" -eq 1 ]] || {
usage
exit 2
}
require_commands kubectl jq rg curl nc python3 awk cmp find bash
bash "${SCRIPT_DIR}/validate.sh"
work_dir="$(mktemp -d "${TMPDIR:-/tmp}/platform-traefik-observe.XXXXXX")"
rollback_required=false
selected_context=""
cleanup() {
local exit_code=$?
trap - EXIT
if [[ "$exit_code" -ne 0 && "$rollback_required" == "true" ]]; then
printf '\nROLLBACK: restoring the durable NodePort-only baseline.\n' >&2
set +e
kubectl --context "$selected_context" apply --kustomize "$BASELINE_OVERLAY"
wait_for_runtime baseline
kubectl --context "$selected_context" --namespace kube-system \
rollout status deployment/traefik --timeout=5m
wait_for_nodeport_boundary_and_health
printf 'ROLLBACK complete. NodePort 30080/30443 remains pinned.\n' >&2
set -e
fi
case "$work_dir" in
/tmp/platform-traefik-observe.*|"${TMPDIR:-/tmp}"/platform-traefik-observe.*)
rm -rf -- "$work_dir"
;;
*)
printf 'WARNING: refusing to remove unexpected directory: %s\n' \
"$work_dir" >&2
;;
esac
exit "$exit_code"
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
observe_render="${work_dir}/observe.yaml"
render_overlay "$OBSERVE_OVERLAY" "$observe_render"
assert_live_baseline
assert_nodeport_boundary_and_health
selected_context="$(current_context)"
selected_api_server="$(current_api_server)"
printf '\nKubernetes context: %s\nAPI server: %s\nTarget node: %s\n' \
"$selected_context" "$selected_api_server" "$TARGET_NODE"
already_applied=false
if kubectl --namespace kube-system \
get helmchartconfig.helm.cattle.io traefik >/dev/null 2>&1; then
assert_live_hcc_matches_overlay "$OBSERVE_OVERLAY" \
"${work_dir}/live-observe-compare.yaml"
already_applied=true
printf 'The live HelmChartConfig already matches the observation overlay.\n'
fi
printf 'Type APPLY %s OBSERVE to enable JSON access logs: ' "$selected_context"
read -r confirmation
[[ "$confirmation" == "APPLY ${selected_context} OBSERVE" ]] || \
fail "cancelled"
[[ "$(current_context)" == "$selected_context" ]] || \
fail "kubectl context changed after confirmation"
[[ "$(current_api_server)" == "$selected_api_server" ]] || \
fail "Kubernetes API server changed after confirmation"
assert_live_baseline
assert_nodeport_boundary_and_health
if [[ "$already_applied" == "false" ]]; then
rollback_required=true
kubectl apply --filename "$observe_render"
fi
wait_for_runtime observe
kubectl --namespace kube-system rollout status deployment/traefik --timeout=5m
assert_runtime observe
assert_live_hcc_matches_overlay "$OBSERVE_OVERLAY" \
"${work_dir}/post-apply-observe.yaml"
assert_live_baseline
wait_for_nodeport_boundary_and_health
manifest_start_url="$(
curl --fail-with-body --silent --show-error \
--resolve "${GITEA_HOST}:443:127.0.0.1" \
"$GITEA_MANIFEST_URL" |
jq --raw-output '.start_url'
)"
rollback_required=false
printf '\nOBSERVATION PHASE READY\n'
printf 'Traefik JSON access logging: enabled\n'
printf 'forwardedHeaders trust: absent\n'
printf 'Current Gitea manifest start_url: %s\n' "$manifest_start_url"
printf 'Next: bash %s/observe-client-host.sh\n' "$SCRIPT_DIR"
+131
View File
@@ -0,0 +1,131 @@
#!/usr/bin/env bash
set -Eeuo pipefail
set +x
umask 077
readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
# shellcheck source=common.sh
source "${SCRIPT_DIR}/common.sh"
usage() {
cat <<'USAGE'
Usage:
bash infrastructure/networking/traefik/scripts/apply-trust.sh \
--observed-client-host <IP> --execute
The IP must be the ClientHost printed by observe-client-host.sh. The trust
overlay must already contain that exact IP as /32 (IPv4) or /128 (IPv6).
USAGE
}
[[ "$#" -eq 3 && "$1" == "--observed-client-host" && "$3" == "--execute" ]] || {
usage
exit 2
}
readonly REVIEWED_CLIENT_HOST="$2"
require_commands kubectl jq rg curl nc python3 awk sort date cmp find bash
reviewed_cidr="$(host_to_exact_cidr "$REVIEWED_CLIENT_HOST")"
declared_cidr="$(source_trusted_proxy_cidr)"
declared_cidr="$(normalize_exact_host_cidr "$declared_cidr")"
[[ "$declared_cidr" != "$SENTINEL_TRUSTED_PROXY_CIDR" ]] || \
fail "trust overlay still contains the non-routable sentinel CIDR"
[[ "$declared_cidr" == "$reviewed_cidr" ]] || \
fail "declared CIDR ${declared_cidr} does not match ClientHost ${REVIEWED_CLIENT_HOST}"
bash "${SCRIPT_DIR}/validate.sh"
work_dir="$(mktemp -d "${TMPDIR:-/tmp}/platform-traefik-trust.XXXXXX")"
rollback_required=false
selected_context=""
cleanup() {
local exit_code=$?
trap - EXIT
if [[ "$exit_code" -ne 0 && "$rollback_required" == "true" ]]; then
printf '\nROLLBACK: restoring the access-log-only observation overlay.\n' >&2
set +e
kubectl --context "$selected_context" apply --kustomize "$OBSERVE_OVERLAY"
wait_for_runtime observe
kubectl --context "$selected_context" --namespace kube-system \
rollout status deployment/traefik --timeout=5m
wait_for_nodeport_boundary_and_health
printf 'ROLLBACK complete. Forwarded-header trust removal was requested.\n' >&2
set -e
fi
case "$work_dir" in
/tmp/platform-traefik-trust.*|"${TMPDIR:-/tmp}"/platform-traefik-trust.*)
rm -rf -- "$work_dir"
;;
*)
printf 'WARNING: refusing to remove unexpected directory: %s\n' \
"$work_dir" >&2
;;
esac
exit "$exit_code"
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
trust_render="${work_dir}/trust.yaml"
render_overlay "$TRUST_OVERLAY" "$trust_render"
assert_live_baseline
assert_runtime observe
assert_live_hcc_matches_overlay "$OBSERVE_OVERLAY" \
"${work_dir}/live-observe.yaml"
assert_nodeport_boundary_and_health
fresh_client_host="$(observe_host_nginx_client_host)"
fresh_cidr="$(host_to_exact_cidr "$fresh_client_host")"
[[ "$fresh_cidr" == "$reviewed_cidr" ]] || \
fail "fresh ClientHost ${fresh_client_host} differs from reviewed ${REVIEWED_CLIENT_HOST}"
selected_context="$(current_context)"
selected_api_server="$(current_api_server)"
printf '\nKubernetes context: %s\nAPI server: %s\nTarget node: %s\n' \
"$selected_context" "$selected_api_server" "$TARGET_NODE"
printf 'Fresh ClientHost: %s\nExact trusted CIDR: %s\n' \
"$fresh_client_host" "$declared_cidr"
printf 'Type APPLY %s TRUST %s to continue: ' \
"$selected_context" "$declared_cidr"
read -r confirmation
[[ "$confirmation" == "APPLY ${selected_context} TRUST ${declared_cidr}" ]] || \
fail "cancelled"
[[ "$(current_context)" == "$selected_context" ]] || \
fail "kubectl context changed after confirmation"
[[ "$(current_api_server)" == "$selected_api_server" ]] || \
fail "Kubernetes API server changed after confirmation"
assert_live_baseline
assert_runtime observe
assert_nodeport_boundary_and_health
assert_live_hcc_matches_overlay "$OBSERVE_OVERLAY" \
"${work_dir}/pre-apply-observe.yaml"
rollback_required=true
kubectl apply --filename "$trust_render"
wait_for_runtime trust "$declared_cidr"
kubectl --namespace kube-system rollout status deployment/traefik --timeout=5m
assert_runtime trust "$declared_cidr"
assert_live_hcc_matches_overlay "$TRUST_OVERLAY" \
"${work_dir}/post-apply-trust.yaml"
assert_live_baseline
wait_for_nodeport_boundary_and_health
assert_manifest_https
rollback_required=false
printf '\nTRUST PHASE READY\n'
printf 'Traefik web trusted CIDR: %s\n' "$declared_cidr"
printf 'Traefik websecure trusted CIDR: absent\n'
printf 'forwardedHeaders.insecure: absent\n'
printf 'Gitea health and HTTPS site-manifest checks: PASS\n'
printf 'Repeat the 30080/30443 refusal check from a separate LAN client.\n'
+464
View File
@@ -0,0 +1,464 @@
#!/usr/bin/env bash
# 이 파일은 같은 디렉터리의 실행 스크립트에서만 source한다.
readonly TRAEFIK_ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd -P)"
readonly BASELINE_OVERLAY="${TRAEFIK_ROOT}/overlays/baseline"
readonly OBSERVE_OVERLAY="${TRAEFIK_ROOT}/overlays/observe"
readonly TRUST_OVERLAY="${TRAEFIK_ROOT}/overlays/trust"
readonly TRUST_PATCH="${TRUST_OVERLAY}/trusted-proxy-cidr-patch.yaml"
readonly SENTINEL_TRUSTED_PROXY_CIDR="192.0.2.1/32"
readonly TARGET_NODE="donghyeon-system-product-name"
readonly K3S_NODEPORT_CONFIG="/etc/rancher/k3s/config.yaml.d/30-nodeport-loopback.yaml"
readonly EXPECTED_K3S_CHART="https://%{KUBERNETES_API}%/static/charts/traefik-40.1.3+up40.1.0.tgz"
readonly EXPECTED_CHART_LABEL="traefik-40.1.3_up40.1.0"
readonly EXPECTED_TRAEFIK_IMAGE="rancher/mirrored-library-traefik:3.7.4"
readonly GITEA_HOST="git.learn.hyeonworks.com"
readonly GITEA_HEALTH_URL="http://127.0.0.1:30080/api/healthz"
readonly GITEA_HTTPS_HEALTH_URL="https://${GITEA_HOST}/api/healthz"
readonly GITEA_MANIFEST_URL="https://${GITEA_HOST}/assets/site-manifest.json"
fail() {
printf 'ERROR: %s\n' "$*" >&2
return 1
}
require_commands() {
local command_name
for command_name in "$@"; do
command -v "$command_name" >/dev/null 2>&1 || \
fail "${command_name} is required"
done
}
render_overlay() {
local overlay="$1"
local output="$2"
kubectl kustomize "$overlay" >"$output"
[[ -s "$output" ]] || fail "rendered manifest is empty: ${overlay}"
}
manifest_values_content() {
local manifest="$1"
awk '
/^ valuesContent: \|-$/ {
found = 1
next
}
found {
sub(/^ /, "")
print
}
' "$manifest"
}
source_trusted_proxy_cidr() {
local -a values=()
mapfile -t values < <(
awk -F'"' '/^[[:space:]]*-[[:space:]]*"/ { print $2 }' "$TRUST_PATCH"
)
[[ "${#values[@]}" -eq 1 ]] || \
fail "trust patch must contain exactly one quoted trusted CIDR"
printf '%s\n' "${values[0]}"
}
host_to_exact_cidr() {
local host="$1"
python3 - "$host" <<'PY'
import ipaddress
import sys
value = sys.argv[1]
if "/" in value:
raise SystemExit("ClientHost must be one IP address, not a CIDR")
address = ipaddress.ip_address(value)
if address.is_unspecified or address.is_multicast:
raise SystemExit("ClientHost cannot be unspecified or multicast")
prefix = 32 if address.version == 4 else 128
print(f"{address.compressed}/{prefix}")
PY
}
normalize_exact_host_cidr() {
local cidr="$1"
python3 - "$cidr" <<'PY'
import ipaddress
import sys
network = ipaddress.ip_network(sys.argv[1], strict=True)
required_prefix = 32 if network.version == 4 else 128
if network.prefixlen != required_prefix:
raise SystemExit(
f"trusted proxy range must be one exact host /{required_prefix}, "
f"not {network.with_prefixlen}"
)
print(network.with_prefixlen)
PY
}
current_context() {
kubectl config current-context
}
current_api_server() {
kubectl config view --minify --output=jsonpath='{.clusters[0].cluster.server}'
}
deployment_args() {
kubectl --namespace kube-system get deployment traefik --output=json |
jq --raw-output '
.spec.template.spec.containers[]
| select(.name == "traefik")
| .args[]
'
}
runtime_matches() {
local mode="$1"
local trusted_cidr="${2:-}"
local args
args="$(deployment_args 2>/dev/null)" || return 1
if rg --quiet --ignore-case -- 'forwardedheaders\.insecure' <<<"$args"; then
return 1
fi
if [[ "$mode" != "baseline" ]]; then
rg --quiet --fixed-strings --line-regexp -- '--accesslog=true' <<<"$args" || \
return 1
rg --quiet --fixed-strings --line-regexp -- '--accesslog.format=json' <<<"$args" || \
return 1
fi
case "$mode" in
observe)
! rg --quiet --ignore-case -- 'forwardedheaders\.trustedips' <<<"$args"
;;
trust)
rg --quiet --fixed-strings --line-regexp -- \
"--entryPoints.web.forwardedHeaders.trustedIPs=${trusted_cidr}" <<<"$args" || \
return 1
! rg --quiet --ignore-case -- \
'entrypoints\.websecure\.forwardedheaders\.(trustedips|insecure)' <<<"$args"
;;
baseline)
! rg --quiet --ignore-case -- \
'accesslog|forwardedheaders\.(trustedips|insecure)' <<<"$args"
;;
*)
return 1
;;
esac
}
assert_runtime() {
local mode="$1"
local trusted_cidr="${2:-}"
local args
local trusted_count
args="$(deployment_args)"
rg --quiet --fixed-strings --line-regexp -- '--accesslog=true' <<<"$args" || \
fail "Traefik runtime is missing --accesslog=true"
rg --quiet --fixed-strings --line-regexp -- '--accesslog.format=json' <<<"$args" || \
fail "Traefik runtime is missing JSON access-log format"
if rg --quiet --ignore-case -- 'forwardedheaders\.insecure' <<<"$args"; then
fail "forwardedHeaders.insecure must never be present"
fi
trusted_count="$(
rg --count --ignore-case -- 'forwardedheaders\.trustedips' <<<"$args" || true
)"
trusted_count="${trusted_count:-0}"
case "$mode" in
observe)
[[ "$trusted_count" == "0" ]] || \
fail "observation phase must not trust forwarded headers"
;;
trust)
[[ "$trusted_count" == "1" ]] || \
fail "trust phase must render exactly one trustedIPs argument"
rg --quiet --fixed-strings --line-regexp -- \
"--entryPoints.web.forwardedHeaders.trustedIPs=${trusted_cidr}" <<<"$args" || \
fail "web entrypoint does not contain the reviewed exact-host CIDR"
if rg --quiet --ignore-case -- \
'entrypoints\.websecure\.forwardedheaders\.(trustedips|insecure)' <<<"$args"; then
fail "websecure must not receive forwarded-header trust"
fi
;;
*)
fail "unsupported runtime assertion mode: ${mode}"
;;
esac
}
wait_for_runtime() {
local mode="$1"
local trusted_cidr="${2:-}"
local attempt
for ((attempt = 1; attempt <= 120; attempt++)); do
if runtime_matches "$mode" "$trusted_cidr"; then
return 0
fi
sleep 2
done
fail "Traefik runtime did not reach ${mode} state within 240 seconds"
}
assert_live_baseline() {
local chart
local chart_label
local image
local nodeport_matches
local service_json
kubectl get node "$TARGET_NODE" >/dev/null
chart="$(
kubectl --namespace kube-system get helmchart.helm.cattle.io traefik \
--output=jsonpath='{.spec.chart}'
)"
[[ "$chart" == "$EXPECTED_K3S_CHART" ]] || \
fail "unexpected packaged Traefik chart: ${chart}"
chart_label="$(
kubectl --namespace kube-system get deployment traefik \
--output=jsonpath='{.metadata.labels.helm\.sh/chart}'
)"
[[ "$chart_label" == "$EXPECTED_CHART_LABEL" ]] || \
fail "unexpected live Traefik chart label: ${chart_label}"
image="$(
kubectl --namespace kube-system get deployment traefik --output=json |
jq --raw-output '
.spec.template.spec.containers[]
| select(.name == "traefik")
| .image
'
)"
[[ "$image" == "$EXPECTED_TRAEFIK_IMAGE" ]] || \
fail "unexpected live Traefik image: ${image}"
[[ -r "$K3S_NODEPORT_CONFIG" ]] || \
fail "cannot read the k3s nodeport-addresses drop-in: ${K3S_NODEPORT_CONFIG}"
rg --quiet --fixed-strings --line-regexp -- \
' - "nodeport-addresses=127.0.0.0/8"' "$K3S_NODEPORT_CONFIG" || \
fail "k3s nodeport-addresses is not pinned to 127.0.0.0/8"
nodeport_matches="$(
rg --no-heading --line-number -- 'nodeport-addresses[=:]' \
/etc/rancher/k3s/config.yaml \
/etc/rancher/k3s/config.yaml.d 2>/dev/null || true
)"
[[ "$(wc -l <<<"$nodeport_matches" | tr -d '[:space:]')" == "1" ]] || \
fail "nodeport-addresses must have exactly one k3s configuration owner"
rg --quiet --fixed-strings -- "$K3S_NODEPORT_CONFIG" <<<"$nodeport_matches" || \
fail "nodeport-addresses is owned by an unexpected k3s configuration file"
service_json="$(
kubectl --namespace kube-system get service traefik --output=json
)"
jq --exit-status '
.spec.type == "NodePort"
and .spec.externalTrafficPolicy == "Cluster"
and (.spec.ports | length) == 2
and any(.spec.ports[];
.name == "web"
and .port == 80
and .nodePort == 30080
and .protocol == "TCP")
and any(.spec.ports[];
.name == "websecure"
and .port == 443
and .nodePort == 30443
and .protocol == "TCP")
' >/dev/null <<<"$service_json" || \
fail "Traefik Service no longer matches the 80/30080 and 443/30443 boundary"
}
assert_health_body() {
local body="$1"
jq --exit-status '
.status == "pass"
and (.checks["database:ping"] | length) > 0
and all(.checks["database:ping"][]; .status == "pass")
and (.checks["cache:ping"] | length) > 0
and all(.checks["cache:ping"][]; .status == "pass")
' >/dev/null <<<"$body" || fail "Gitea database/cache health is not pass"
}
assert_nodeport_boundary_and_health() {
local body
local node_ip
local port
node_ip="$(
kubectl get node "$TARGET_NODE" --output=json |
jq --raw-output '
[.status.addresses[] | select(.type == "InternalIP") | .address]
| if length == 1 then .[0] else empty end
'
)"
[[ -n "$node_ip" && "$node_ip" != "127.0.0.1" ]] || \
fail "could not resolve exactly one non-loopback node InternalIP"
for port in 30080 30443; do
nc -z -w 3 127.0.0.1 "$port" >/dev/null 2>&1 || \
fail "loopback NodePort is not listening: 127.0.0.1:${port}"
if nc -z -w 3 "$node_ip" "$port" >/dev/null 2>&1; then
fail "NodePort escaped the loopback boundary: ${node_ip}:${port}"
fi
done
body="$(
curl --fail-with-body --silent --show-error \
--header "Host: ${GITEA_HOST}" \
"$GITEA_HEALTH_URL"
)"
assert_health_body "$body"
body="$(
curl --fail-with-body --silent --show-error \
--resolve "${GITEA_HOST}:443:127.0.0.1" \
"$GITEA_HTTPS_HEALTH_URL"
)"
assert_health_body "$body"
}
wait_for_nodeport_boundary_and_health() {
local attempt
for ((attempt = 1; attempt <= 60; attempt++)); do
if assert_nodeport_boundary_and_health >/dev/null 2>&1; then
return 0
fi
sleep 2
done
# 마지막 검사는 오류 원인을 숨기지 않고 그대로 출력한다.
assert_nodeport_boundary_and_health
fail "Traefik NodePort boundary and Gitea health did not recover within 120 seconds"
}
observe_host_nginx_client_host() {
local http_code
local logs
local matched
local nonce
local observed
local probe_path
local router_count
local since
local -a client_hosts=()
local attempt
since="$(date --utc '+%Y-%m-%dT%H:%M:%SZ')"
nonce="$(date --utc '+%Y%m%dT%H%M%S')-${BASHPID}"
probe_path="/api/healthz/traefik-source-${nonce}"
http_code="$(
curl --silent --show-error \
--resolve "${GITEA_HOST}:443:127.0.0.1" \
--output /dev/null \
--write-out '%{http_code}' \
"https://${GITEA_HOST}${probe_path}"
)"
[[ "$http_code" == "404" ]] || \
fail "unique Host Nginx observation request returned HTTP ${http_code}, expected 404"
matched=""
for ((attempt = 1; attempt <= 20; attempt++)); do
logs="$(
kubectl --namespace kube-system logs deployment/traefik \
--since-time "$since"
)"
matched="$(
jq --raw-input --compact-output --arg path "$probe_path" '
fromjson?
| select(.RequestPath == $path)
' <<<"$logs"
)"
[[ -n "$matched" ]] && break
sleep 1
done
[[ -n "$matched" ]] || \
fail "the unique request was not found in Traefik JSON access logs"
router_count="$(
jq --slurp '
[
.[]
| select(
((.RouterName // "") | ascii_downcase | contains("gitea"))
)
]
| length
' <<<"$matched"
)"
[[ "$router_count" -ge 1 ]] || \
fail "the observation log did not traverse a Gitea router"
mapfile -t client_hosts < <(
jq --raw-output '
select((.RouterName // "") | ascii_downcase | contains("gitea"))
| .ClientHost // empty
' <<<"$matched" |
sort --unique
)
[[ "${#client_hosts[@]}" -eq 1 && -n "${client_hosts[0]}" ]] || \
fail "expected one distinct Traefik ClientHost for the unique request"
observed="${client_hosts[0]}"
host_to_exact_cidr "$observed" >/dev/null
printf '%s\n' "$observed"
}
assert_manifest_https() {
local body
local attempt
for ((attempt = 1; attempt <= 30; attempt++)); do
body="$(
curl --fail-with-body --silent --show-error \
--resolve "${GITEA_HOST}:443:127.0.0.1" \
"$GITEA_MANIFEST_URL"
)"
if jq --exit-status --arg expected "https://${GITEA_HOST}/" '
.start_url == $expected
and all(.icons[]; (.src | startswith("https://")))
' >/dev/null <<<"$body"; then
return 0
fi
sleep 2
done
fail "Gitea site manifest did not stabilize on HTTPS URLs"
}
assert_live_hcc_matches_overlay() {
local overlay="$1"
local render_file="$2"
local actual
local expected
render_overlay "$overlay" "$render_file"
expected="$(manifest_values_content "$render_file")"
actual="$(
kubectl --namespace kube-system \
get helmchartconfig.helm.cattle.io traefik --output=json |
jq --raw-output '.spec.valuesContent'
)"
[[ "$actual" == "$expected" ]] || \
fail "live Traefik HelmChartConfig does not match the expected overlay"
}
@@ -0,0 +1,58 @@
#!/usr/bin/env bash
set -Eeuo pipefail
set +x
umask 077
readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
# shellcheck source=common.sh
source "${SCRIPT_DIR}/common.sh"
usage() {
cat <<'USAGE'
Usage: bash infrastructure/networking/traefik/scripts/observe-client-host.sh
Sends one unique HTTPS request through Host Nginx and extracts the corresponding
ClientHost from Traefik JSON access logs. It performs no cluster mutation.
USAGE
}
[[ "$#" -eq 0 ]] || {
usage
exit 2
}
require_commands kubectl jq rg curl nc python3 awk sort date
work_dir="$(mktemp -d "${TMPDIR:-/tmp}/platform-traefik-clienthost.XXXXXX")"
cleanup() {
case "$work_dir" in
/tmp/platform-traefik-clienthost.*|"${TMPDIR:-/tmp}"/platform-traefik-clienthost.*)
rm -rf -- "$work_dir"
;;
*)
printf 'WARNING: refusing to remove unexpected directory: %s\n' \
"$work_dir" >&2
;;
esac
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
bash "${SCRIPT_DIR}/validate.sh"
assert_live_baseline
assert_runtime observe
assert_live_hcc_matches_overlay "$OBSERVE_OVERLAY" \
"${work_dir}/live-observe.yaml"
assert_nodeport_boundary_and_health
observed_client_host="$(observe_host_nginx_client_host)"
trusted_proxy_cidr="$(host_to_exact_cidr "$observed_client_host")"
printf '\nTRAEFIK SOURCE OBSERVED\n'
printf 'ClientHost: %s\n' "$observed_client_host"
printf 'Minimum trusted CIDR: %s\n' "$trusted_proxy_cidr"
printf 'Record that CIDR in:\n%s\n' "$TRUST_PATCH"
printf 'Replace only the sentinel %s, then run apply-trust.sh with this ClientHost.\n' \
"$SENTINEL_TRUSTED_PROXY_CIDR"
@@ -0,0 +1,75 @@
#!/usr/bin/env bash
set -Eeuo pipefail
set +x
umask 077
readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
# shellcheck source=common.sh
source "${SCRIPT_DIR}/common.sh"
usage() {
cat <<'USAGE'
Usage:
bash infrastructure/networking/traefik/scripts/rollback-to-observe.sh --execute
Removes forwarded-header trust while retaining JSON access logging.
USAGE
}
[[ "${1:-}" == "--execute" && "$#" -eq 1 ]] || {
usage
exit 2
}
require_commands kubectl jq rg curl nc python3 awk cmp find bash
bash "${SCRIPT_DIR}/validate.sh"
assert_live_baseline
kubectl --namespace kube-system \
get helmchartconfig.helm.cattle.io traefik >/dev/null
assert_nodeport_boundary_and_health
selected_context="$(current_context)"
selected_api_server="$(current_api_server)"
printf '\nKubernetes context: %s\nAPI server: %s\nTarget node: %s\n' \
"$selected_context" "$selected_api_server" "$TARGET_NODE"
printf 'Type ROLLBACK %s OBSERVE to remove forwarded-header trust: ' \
"$selected_context"
read -r confirmation
[[ "$confirmation" == "ROLLBACK ${selected_context} OBSERVE" ]] || \
fail "cancelled"
[[ "$(current_context)" == "$selected_context" ]] || \
fail "kubectl context changed after confirmation"
[[ "$(current_api_server)" == "$selected_api_server" ]] || \
fail "Kubernetes API server changed after confirmation"
assert_live_baseline
kubectl apply --kustomize "$OBSERVE_OVERLAY"
wait_for_runtime observe
kubectl --namespace kube-system rollout status deployment/traefik --timeout=5m
assert_runtime observe
work_dir="$(mktemp -d "${TMPDIR:-/tmp}/platform-traefik-rollback.XXXXXX")"
cleanup() {
case "$work_dir" in
/tmp/platform-traefik-rollback.*|"${TMPDIR:-/tmp}"/platform-traefik-rollback.*)
rm -rf -- "$work_dir"
;;
*)
printf 'WARNING: refusing to remove unexpected directory: %s\n' \
"$work_dir" >&2
;;
esac
}
trap cleanup EXIT
assert_live_hcc_matches_overlay "$OBSERVE_OVERLAY" \
"${work_dir}/post-rollback-observe.yaml"
assert_live_baseline
wait_for_nodeport_boundary_and_health
printf '\nROLLBACK COMPLETE\n'
printf 'JSON access logging remains enabled.\n'
printf 'forwardedHeaders trust is absent from both entrypoints.\n'
+161
View File
@@ -0,0 +1,161 @@
#!/usr/bin/env bash
set -Eeuo pipefail
set +x
umask 077
readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
# shellcheck source=common.sh
source "${SCRIPT_DIR}/common.sh"
assert_count() {
local file="$1"
local pattern="$2"
local expected="$3"
local description="$4"
local actual
actual="$(rg --count --no-filename -- "$pattern" "$file" || true)"
actual="${actual:-0}"
[[ "$actual" == "$expected" ]] || \
fail "${description}: expected ${expected}, found ${actual}"
}
require_commands kubectl rg awk cmp python3 find bash
render_dir="$(mktemp -d "${TMPDIR:-/tmp}/platform-traefik-render.XXXXXX")"
cleanup() {
case "$render_dir" in
/tmp/platform-traefik-render.*|"${TMPDIR:-/tmp}"/platform-traefik-render.*)
rm -rf -- "$render_dir"
;;
*)
printf 'WARNING: refusing to remove unexpected directory: %s\n' \
"$render_dir" >&2
;;
esac
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
root_render="${render_dir}/root.yaml"
baseline_render="${render_dir}/baseline.yaml"
observe_render="${render_dir}/observe.yaml"
trust_render="${render_dir}/trust.yaml"
render_overlay "$TRAEFIK_ROOT" "$root_render"
render_overlay "$BASELINE_OVERLAY" "$baseline_render"
render_overlay "$OBSERVE_OVERLAY" "$observe_render"
render_overlay "$TRUST_OVERLAY" "$trust_render"
cmp --silent "$root_render" "$observe_render" || \
fail "the Traefik root must render the observation phase"
for manifest in "$baseline_render" "$observe_render" "$trust_render"; do
assert_count "$manifest" '^apiVersion: helm\.cattle\.io/v1$' 1 \
"HelmChartConfig API version"
assert_count "$manifest" '^kind: HelmChartConfig$' 1 \
"HelmChartConfig kind"
assert_count "$manifest" '^ name: traefik$' 1 \
"HelmChartConfig name"
assert_count "$manifest" '^ namespace: kube-system$' 1 \
"HelmChartConfig namespace"
assert_count "$manifest" '^ failurePolicy: abort$' 1 \
"Helm failure policy must preserve the running release on upgrade failure"
assert_count "$manifest" '^ deployment:$' 1 \
"Traefik Deployment values root"
assert_count "$manifest" '^ podAnnotations:$' 1 \
"Traefik Pod annotations block"
assert_count "$manifest" '^ prometheus\.io/path: /metrics$' 1 \
"Traefik legacy scrape path preservation"
assert_count "$manifest" '^ prometheus\.io/port: "9100"$' 1 \
"Traefik legacy scrape port preservation"
assert_count "$manifest" '^ prometheus\.io/scrape: "true"$' 1 \
"Traefik legacy scrape enablement preservation"
assert_count "$manifest" '[Ii]nsecure' 0 \
"insecure forwarded-header mode"
assert_count "$manifest" '^ service:$' 1 \
"Traefik Service values root"
assert_count "$manifest" '^ type: NodePort$' 1 \
"loopback NodePort Service type preservation"
assert_count "$manifest" '^ externalTrafficPolicy: Cluster$' 1 \
"Traefik externalTrafficPolicy preservation"
assert_count "$manifest" '^ nodePort: 30080$' 1 \
"Traefik web NodePort preservation"
assert_count "$manifest" '^ nodePort: 30443$' 1 \
"Traefik websecure NodePort preservation"
assert_count "$manifest" '^ metrics:$' 1 \
"Traefik metrics values root"
assert_count "$manifest" '^ prometheus:$' 1 \
"Traefik Prometheus metrics block"
assert_count "$manifest" '^ serviceMonitor:$' 1 \
"Traefik ServiceMonitor block"
assert_count "$manifest" '^ jobLabel: app\.kubernetes\.io/name$' 1 \
"Traefik ServiceMonitor job label"
assert_count "$manifest" '^ observability\.hyeonworks\.com/instance: home$' 1 \
"Traefik ServiceMonitor selector label"
assert_count "$manifest" '^ interval: 30s$' 1 \
"Traefik ServiceMonitor interval"
assert_count "$manifest" '^ scrapeTimeout: 10s$' 1 \
"Traefik ServiceMonitor timeout"
assert_count "$manifest" '^ enabled: true$' 2 \
"Traefik metrics Service and ServiceMonitor enablement"
assert_count "$manifest" '^[[:space:]]*(type|serviceType):[[:space:]]*LoadBalancer' 0 \
"LoadBalancer exposure"
done
assert_count "$baseline_render" '^[[:space:]]*logs:' 0 \
"baseline access logs"
assert_count "$baseline_render" '^[[:space:]]*forwardedHeaders:' 0 \
"baseline forwarded-header trust"
for manifest in "$observe_render" "$trust_render"; do
assert_count "$manifest" '^ enabled: true$' 1 \
"access log enablement"
assert_count "$manifest" '^ format: json$' 1 \
"JSON access log format"
assert_count "$manifest" '^ defaultmode: keep$' 1 \
"access-log general-field policy"
assert_count "$manifest" '^ defaultmode: drop$' 1 \
"access-log header policy"
done
assert_count "$observe_render" '^[[:space:]]*forwardedHeaders:' 0 \
"observation-phase forwarded-header trust"
assert_count "$observe_render" '^[[:space:]]*trustedIPs:' 0 \
"observation-phase trusted IP list"
assert_count "$trust_render" '^ ports:$' 1 \
"trust-phase ports values root"
assert_count "$trust_render" '^ web:$' 1 \
"trust-phase web entrypoint"
assert_count "$trust_render" '^ forwardedHeaders:$' 1 \
"trust-phase forwarded-header block"
assert_count "$trust_render" '^ trustedIPs:$' 1 \
"trust-phase trusted IP list"
trusted_cidr="$(source_trusted_proxy_cidr)"
normalized_cidr="$(normalize_exact_host_cidr "$trusted_cidr")"
[[ "$trusted_cidr" == "$normalized_cidr" ]] || \
fail "trusted proxy CIDR must use canonical exact-host notation"
assert_count "$trust_render" \
"^[[:space:]]*-[[:space:]]*\"${trusted_cidr//./\\.}\"[[:space:]]*$" 1 \
"rendered exact-host trusted proxy CIDR"
while IFS= read -r -d '' script_path; do
bash -n "$script_path"
done < <(
find "$SCRIPT_DIR" -maxdepth 1 -type f -name '*.sh' -print0
)
printf 'Traefik observation and trust overlays rendered successfully.\n'
printf 'Access logs are JSON and request headers are dropped.\n'
printf 'No insecure mode, websecure trust, or LoadBalancer exposure was found.\n'
printf 'The existing NodePort 30080/30443 boundary is declared in both phases.\n'
if [[ "$trusted_cidr" == "$SENTINEL_TRUSTED_PROXY_CIDR" ]]; then
printf 'Trust overlay remains intentionally blocked by sentinel CIDR %s.\n' \
"$SENTINEL_TRUSTED_PROXY_CIDR"
else
printf 'Trust overlay contains reviewed exact-host CIDR %s.\n' "$trusted_cidr"
fi