mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-12 09:30:46 +02:00
* terraform: add cloud-agnostic core renderer module Renders per-node weed argv, systemd units, config files, disk-mount and secret-fetch scripts, and cloud-init from an address map. Creates zero cloud resources. Flags verified against the weed binary: volume uses -mserver for the master list, gRPC is -port.grpc (auto http+10000), minFreeSpacePercent is a string, filer store via -defaultStoreDir. * terraform: add mTLS and JWT security module Generates the CA, per-component certs with distinct CNs, and JWT signing keys via the tls/random providers. Emits a core_security object plus PEMs for secret-store delivery. * terraform: add AWS deployment module and examples Reserves stable ENIs first, renders config via the core, then creates instances, prevent_destroy EBS data disks mounted at /data, and the cluster security group. With enable_security, generates certs/JWT, stores them in SSM SecureString, grants an instance role, and fetches them at boot so secrets stay out of user_data. Keyed for_each on every stateful tier. * terraform: add local cluster test harnesses run_local_cluster.sh and run_local_secure.sh render a cluster with the core and run real weed processes, asserting master quorum, volume registration, filer/s3 round-trips, mutual-TLS formation, and JWT enforcement. Use an isolated high port range with a guard so they never touch a cluster already running on the machine. The weed binary defaults to $(go env GOPATH)/bin/weed. * terraform: add CI workflow and README fmt/validate/tofu-test plus smoke jobs that build weed and run both harnesses. * terraform: guard against empty filesystem UUID in mount script An empty UUID made grep -q match any fstab line, skipping the fstab entry and breaking the mount. Fail fast when blkid returns no UUID. * terraform: sanitize cluster name in WEED_CLUSTER env keys Hyphens or spaces in cluster_name produced invalid systemd/bash env var names; map non-alphanumerics to underscores. * terraform: omit empty jwt.signing block from security.toml With enable_security and no JWT key, the template emitted [jwt.signing] key="". Gate the block on a non-empty key and cover it with a test. * terraform: mark core security input as sensitive The security object carries JWT signing keys; keep them out of plan output and known values. * terraform: enforce jwt_length minimum of 32 * terraform: note region/AZ coupling in HA example * terraform: guard WORKDIR before recursive delete in test harnesses * terraform: fix README fence language and test count * terraform: handle embedded s3 with no filer nodes Indexing sort(keys(var.filers))[0] errored at plan time when embedded S3 was enabled but no filers were defined; fall back to an empty config source. * terraform: scope kms:Decrypt to a configurable key arn Replace the hardcoded Resource="*" with a kms_key_arn variable (default "*") so production can restrict decrypt to a specific CMK. * terraform: encrypt EBS data volumes at rest Set encrypted = true on the volume/filer data disks and the all-in-one example disk. * terraform: protect filer instances from API termination Filers hold the leveldb2 metadata store, so they are stateful and get the same disable_api_termination as masters and volumes. * terraform: stop instance before detaching in all-in-one example * terraform: drop stale references to the removed plan doc * terraform: correct stale mount-step comment in aws module * terraform: mark Terraform support as experimental in README
129 lines
5.1 KiB
Bash
Executable File
129 lines
5.1 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Generate mTLS material + security.toml with Terraform, then run a real
|
|
# master+volume+filer cluster with mTLS enabled and assert it works.
|
|
# ./run_local_secure.sh # render + run + assert + teardown
|
|
# KEEP=1 ./run_local_secure.sh
|
|
set -u
|
|
|
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
export PATH="/opt/homebrew/bin:$PATH"
|
|
TOFU="${TOFU:-tofu}"
|
|
WEED="${WEED:-$(go env GOPATH 2>/dev/null || echo "$HOME/go")/bin/weed}"
|
|
WORKDIR="${WORKDIR:-/tmp/seaweedfs-tftest-secure}"
|
|
LOGDIR="$WORKDIR/logs"
|
|
RUNDIR="$WORKDIR/run"
|
|
|
|
PASS=0
|
|
FAIL=0
|
|
ok() { echo " PASS: $1"; PASS=$((PASS + 1)); }
|
|
bad() { echo " FAIL: $1"; FAIL=$((FAIL + 1)); }
|
|
info() { echo "==> $1"; }
|
|
|
|
cleanup() {
|
|
info "tearing down"
|
|
if [ -d "$RUNDIR" ]; then
|
|
for pf in "$RUNDIR"/*.pid; do [ -f "$pf" ] && kill "$(cat "$pf")" 2>/dev/null; done
|
|
sleep 1
|
|
for pf in "$RUNDIR"/*.pid; do [ -f "$pf" ] && kill -9 "$(cat "$pf")" 2>/dev/null; done
|
|
fi
|
|
}
|
|
|
|
info "cleaning $WORKDIR"
|
|
case "$WORKDIR" in "" | "/" | "$HOME") echo "refusing to delete '$WORKDIR'" >&2; exit 2 ;; esac
|
|
rm -rf "$WORKDIR"
|
|
mkdir -p "$LOGDIR" "$RUNDIR" "$WORKDIR/.seaweedfs"
|
|
[ -x "$WEED" ] || { echo "weed not found at $WEED" >&2; exit 2; }
|
|
|
|
info "generating certs + rendering config with OpenTofu"
|
|
cd "$HERE"
|
|
"$TOFU" init -backend=false -input=false -no-color >/dev/null 2>&1 || { echo "tofu init failed"; exit 2; }
|
|
if ! "$TOFU" apply -auto-approve -input=false -no-color \
|
|
-var "weed_binary=$WEED" -var "workdir=$WORKDIR" >"$LOGDIR/tofu.log" 2>&1; then
|
|
echo "tofu apply failed"; tail -30 "$LOGDIR/tofu.log"; exit 2
|
|
fi
|
|
|
|
# write certs to their on-host paths
|
|
"$TOFU" output -json certs | jq -c '.[]' | while IFS= read -r c; do
|
|
p="$(echo "$c" | jq -r '.path')"; m="$(echo "$c" | jq -r '.mode')"
|
|
mkdir -p "$(dirname "$p")"
|
|
echo "$c" | jq -r '.content' > "$p"
|
|
chmod "$m" "$p"
|
|
done
|
|
# place security.toml where weed searches ($HOME/.seaweedfs)
|
|
"$TOFU" output -raw security_toml > "$WORKDIR/.seaweedfs/security.toml"
|
|
info "security.toml + $(ls "$WORKDIR"/certs | wc -l | tr -d ' ') cert dirs written"
|
|
|
|
OUT="$("$TOFU" output -json cluster)"
|
|
|
|
# derive ports from the rendered config (high range; never hardcoded)
|
|
port_of() { echo "$OUT" | jq -r --arg r "$1" '.[] | select(.role==$r) | .http_port' | head -1; }
|
|
MPORT="$(port_of master)"; VPORT="$(port_of volume)"; FPORT="$(port_of filer)"
|
|
|
|
busy=""
|
|
for p in $(echo "$OUT" | jq -r '.[].http_port'); do
|
|
lsof -nP -iTCP:"$p" -sTCP:LISTEN >/dev/null 2>&1 && busy="$busy $p"
|
|
done
|
|
[ -n "$busy" ] && { echo "Required port(s) already in use:$busy -- is another SeaweedFS running?" >&2; exit 3; }
|
|
|
|
[ "${KEEP:-0}" = "1" ] || trap cleanup EXIT INT TERM
|
|
|
|
# launch a node with HOME pointed at $WORKDIR so weed loads security.toml
|
|
launch() {
|
|
n="$1"
|
|
for d in $(echo "$OUT" | jq -r --arg n "$n" '.[$n].data_dirs[]?'); do mkdir -p "$d"; done
|
|
ENVS=("HOME=$WORKDIR")
|
|
while IFS= read -r e; do [ -n "$e" ] && ENVS+=("$e"); done \
|
|
< <(echo "$OUT" | jq -r --arg n "$n" '.[$n].env | to_entries[] | "\(.key)=\(.value)"')
|
|
ARGV=()
|
|
while IFS= read -r a; do ARGV+=("$a"); done \
|
|
< <(echo "$OUT" | jq -r --arg n "$n" '.[$n].argv[]')
|
|
info "launching $n (mTLS)"
|
|
env "${ENVS[@]}" "$WEED" "${ARGV[@]}" >"$LOGDIR/$n.log" 2>&1 &
|
|
echo "$!" > "$RUNDIR/$n.pid"
|
|
}
|
|
|
|
wait_http() {
|
|
url="$1"; t="${2:-30}"; i=0
|
|
while [ "$i" -lt "$t" ]; do
|
|
curl -fsS -o /dev/null --max-time 2 "$url" 2>/dev/null && return 0
|
|
i=$((i + 1)); sleep 1
|
|
done
|
|
return 1
|
|
}
|
|
|
|
launch master-m0
|
|
QOK=0; i=0
|
|
while [ "$i" -lt 40 ]; do
|
|
st="$(curl -fsS --max-time 2 "http://127.0.0.1:$MPORT/cluster/status" 2>/dev/null)" || { i=$((i+1)); sleep 1; continue; }
|
|
[ "$(echo "$st" | jq -r '.IsLeader // false')" = "true" ] && { QOK=1; break; }
|
|
i=$((i + 1)); sleep 1
|
|
done
|
|
[ "$QOK" -eq 1 ] && ok "master elected leader under mTLS" || bad "master leader (mTLS)"
|
|
|
|
launch volume-v0
|
|
# registration (master gRPC over mTLS) confirms the volume joined the cluster
|
|
AOK=0; i=0
|
|
while [ "$i" -lt 40 ]; do
|
|
fid="$(curl -fsS --max-time 2 "http://127.0.0.1:$MPORT/dir/assign" 2>/dev/null | jq -r '.fid // ""')"
|
|
[ -n "$fid" ] && { AOK=1; break; }
|
|
i=$((i + 1)); sleep 1
|
|
done
|
|
[ "$AOK" -eq 1 ] && ok "volume registered via mTLS gRPC (fid $fid)" || bad "volume registration over mTLS"
|
|
# /healthz flips to 200 after the first heartbeat completes (slower under mTLS)
|
|
wait_http "http://127.0.0.1:$VPORT/healthz" 60 && ok "volume /healthz up (mTLS)" || bad "volume /healthz"
|
|
|
|
launch filer-f0
|
|
wait_http "http://127.0.0.1:$FPORT/" 30 && ok "filer / up (mTLS)" || bad "filer /"
|
|
# [jwt.filer_signing] is active, so the filer requires a signed JWT for writes.
|
|
# An unsigned write MUST be rejected with 401 -- this proves the JWT signing key
|
|
# rendered into security.toml is enforced (positive security assertion).
|
|
echo "smoke" > "$WORKDIR/hello.txt"
|
|
code="$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 -X POST \
|
|
-F "file=@$WORKDIR/hello.txt" "http://127.0.0.1:$FPORT/smoke/hello.txt" 2>/dev/null)"
|
|
[ "$code" = "401" ] && ok "filer rejects unsigned write (HTTP 401) => JWT signing enforced" \
|
|
|| bad "filer JWT enforcement (expected 401, got HTTP $code)"
|
|
|
|
echo
|
|
info "RESULTS: $PASS passed, $FAIL failed"
|
|
[ "$FAIL" -eq 0 ]
|