Connect local SXCHNGE to the Sendico stand
This commit was merged in pull request #53.
This commit is contained in:
1 parent
34d8ef18d1
commit
a5689876ff
14 files changed
+336
-9
No files matched your search
@@ -54,24 +54,31 @@ provision_mongo() {
|
||||
{ cat "$WORK/admin.js"; cat infra/local/mongo/mongo-client.js; } | mongo >/dev/null || fail 'Browser client provisioning failed'
|
||||
}
|
||||
provision_agents() {
|
||||
for service in bff notifications nats; do
|
||||
services='bff notifications nats'
|
||||
if [ -f "$STATE/sendico/settings.env" ]; then services="$services sendico"; fi
|
||||
for service in $services; do
|
||||
directory="$STATE/agents/$service"
|
||||
mkdir -p "$directory"; chmod 755 "$directory"
|
||||
case "$service" in
|
||||
bff) paths='services/bff/jwt';;
|
||||
notifications) paths='infrastructure/nats/auth services/notifications/email/resend';;
|
||||
nats) paths=infrastructure/nats/auth;;
|
||||
sendico) paths=services/bff/sendico;;
|
||||
esac
|
||||
: > "$WORK/policy"
|
||||
for path in $paths; do printf 'path "kv/data/%s" { capabilities = ["read"] }\n' "$path" >> "$WORK/policy"; done
|
||||
if [ "$service" != nats ]; then printf 'path "database/creds/%s" { capabilities = ["read"] }\n' "$service" >> "$WORK/policy"; fi
|
||||
case "$service" in bff|notifications) printf 'path "database/creds/%s" { capabilities = ["read"] }\n' "$service" >> "$WORK/policy";; esac
|
||||
jq -n --rawfile policy "$WORK/policy" '{policy:$policy}' | vault "sys/policies/acl/$service" POST >/dev/null
|
||||
jq -n --arg service "$service" '{token_policies:[$service],token_period:"1h",secret_id_ttl:"0",secret_id_num_uses:0}' | vault "auth/approle/role/$service" POST >/dev/null
|
||||
if [ "$service" = sendico ]; then
|
||||
printf '{"token_policies":["sendico"],"token_ttl":"5m","token_max_ttl":"5m","secret_id_ttl":"0","secret_id_num_uses":0}' | vault "auth/approle/role/$service" POST >/dev/null
|
||||
else
|
||||
jq -n --arg service "$service" '{token_policies:[$service],token_period:"1h",secret_id_ttl:"0",secret_id_num_uses:0}' | vault "auth/approle/role/$service" POST >/dev/null
|
||||
fi
|
||||
vault "auth/approle/role/$service/role-id" | jq -r '.data.role_id' | write "$directory/role-id" 444
|
||||
if [ ! -f "$directory/secret-id" ]; then
|
||||
printf '{}' | vault "auth/approle/role/$service/secret-id" POST | jq -r '.data.secret_id' | write "$directory/secret-id" 444
|
||||
fi
|
||||
if ! cmp -s "infra/local/vault/$service.hcl" "$directory/config.hcl"; then
|
||||
if [ "$service" != sendico ] && ! cmp -s "infra/local/vault/$service.hcl" "$directory/config.hcl"; then
|
||||
cat "infra/local/vault/$service.hcl" | write "$directory/config.hcl" 444
|
||||
if compose ps --status running --services "$service" | grep -qx "$service"; then
|
||||
compose up -d --no-deps --force-recreate --wait --wait-timeout 120 "$service"
|
||||
|
||||
@@ -14,7 +14,13 @@ trap cleanup EXIT
|
||||
trap 'exit 130' INT
|
||||
trap 'exit 143' TERM
|
||||
fail() { echo "$*" >&2; exit 1; }
|
||||
compose() { docker compose --project-directory . -f infra/local/compose.yml --env-file "$STATE/images.env" "$@"; }
|
||||
compose() {
|
||||
if [ -f "$STATE/sendico/settings.env" ]; then
|
||||
docker compose --project-directory . -f infra/local/compose.yml -f infra/local/sendico/compose.yml --env-file "$STATE/images.env" "$@"
|
||||
else
|
||||
docker compose --project-directory . -f infra/local/compose.yml --env-file "$STATE/images.env" "$@"
|
||||
fi
|
||||
}
|
||||
write() {
|
||||
target=$1; mode=${2:-600}
|
||||
mkdir -p "$(dirname "$target")"
|
||||
|
||||
@@ -7,6 +7,7 @@ case "${1:-}" in
|
||||
images) . infra/local/scripts/images.sh; images;;
|
||||
up) . infra/local/scripts/bootstrap.sh; up;;
|
||||
email) . infra/local/scripts/bootstrap.sh; email;;
|
||||
sendico) . infra/local/scripts/bootstrap.sh; . infra/local/scripts/sendico.sh; configure_sendico;;
|
||||
verify|verify-infra) . infra/local/scripts/verify.sh; existing; infrastructure; if [ "$1" = verify ]; then product; fi;;
|
||||
rotation) . infra/local/scripts/verify.sh; . infra/local/scripts/rotation.sh; existing; rotation;;
|
||||
test) sh infra/local/scripts/test.sh;;
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
# Operator input is JSON on stdin. Only credential values are stored in Vault.
|
||||
configure_sendico() {
|
||||
existing
|
||||
cat > "$WORK/input"
|
||||
jq -e '
|
||||
(.organization_ref|test("^[0-9a-f]{24}$") and .!="000000000000000000000000") and
|
||||
(.source_account_ref|test("^[0-9a-f]{24}$") and .!="000000000000000000000000") and
|
||||
([.client_id,.device_id,.login,.password,.client_secret] | all(.[]; type=="string" and test("\\S"))) and
|
||||
([.client_id,.device_id] | all(.[]; test("^[A-Za-z0-9._-]+$")))
|
||||
' "$WORK/input" >/dev/null || fail 'Invalid Sendico operator input'
|
||||
jq '{login,password,client_secret}' "$WORK/input" | put services/bff/sendico
|
||||
mkdir -p "$STATE/sendico"
|
||||
jq -r '"SENDICO_ORGANIZATION_REF="+.organization_ref+"\nSENDICO_SOURCE_ACCOUNT_REF="+.source_account_ref+"\nSENDICO_CLIENT_ID="+.client_id+"\nSENDICO_DEVICE_ID="+.device_id' "$WORK/input" | write "$STATE/sendico/settings.env"
|
||||
{
|
||||
cat infra/local/bff/runtime.yml
|
||||
jq -r '"\n sendico:\n api_url: http://127.0.0.1:18081/api/v1\n organization_ref: "+.organization_ref+"\n source_account_ref: "+.source_account_ref+"\n token_file: /run/sendico/access-token"' "$WORK/input"
|
||||
} | write "$STATE/sendico/runtime.yml" 644
|
||||
provision_agents
|
||||
echo 'Local Sendico connection configured. Run make up.'
|
||||
}
|
||||
@@ -4,4 +4,12 @@ umask 077
|
||||
if [ "$SXCHNGE_SERVICE" = notifications ]; then
|
||||
exec service -config.file=/etc/sxchnge/notifications.yml
|
||||
fi
|
||||
if [ "${SXCHNGE_SENDICO_LOCAL:-}" = 1 ]; then
|
||||
attempt=0
|
||||
until [ -s /run/sendico/access-token ] && [ "$(($(date +%s) - $(stat -c %Y /run/sendico/access-token)))" -lt 1500 ]; do
|
||||
attempt=$((attempt+1))
|
||||
[ "$attempt" -lt 120 ] || { echo 'Local Sendico session did not become ready' >&2; exit 1; }
|
||||
sleep 2
|
||||
done
|
||||
fi
|
||||
exec service -config.file=/etc/sxchnge/bff.yml
|
||||
@@ -4,6 +4,7 @@ set -o pipefail
|
||||
# Shell syntax and static analysis cover the actual container shell scripts.
|
||||
shellcheck -s ash -x -S warning infra/local/scripts/dispatch.sh infra/local/scripts/test.sh
|
||||
shellcheck -x -S warning infra/local/tools.sh infra/local/scripts/start-service.sh infra/local/mongo/start.sh infra/local/nats/start.sh infra/local/vault/start.sh
|
||||
shellcheck -s ash -x -S warning infra/local/sendico/session.sh infra/local/sendico/check.sh
|
||||
. infra/local/scripts/common.sh
|
||||
. infra/local/scripts/images.sh
|
||||
. infra/local/scripts/bootstrap.sh
|
||||
@@ -93,3 +94,23 @@ retire_secrets
|
||||
retire_secrets
|
||||
jq -e '.=={"infrastructure/nats/auth":true}' "$WORK/retirement" >/dev/null
|
||||
echo 'PASS: obsolete caller secrets retired idempotently; NATS authentication preserved'
|
||||
# Local integration configuration keeps credentials out of runtime settings and
|
||||
# rejects malformed operator inputs before mutating Vault or service identities.
|
||||
. infra/local/scripts/sendico.sh
|
||||
STATE="$WORK/sendico-state"; mkdir "$STATE"
|
||||
existing() { :; }
|
||||
put() { cat > "$WORK/sendico-credentials"; }
|
||||
provision_agents() { touch "$WORK/sendico-agents"; }
|
||||
printf '{}' > "$WORK/sendico-invalid"
|
||||
if (configure_sendico < "$WORK/sendico-invalid") >/dev/null 2>&1; then fail 'Invalid Sendico configuration accepted'; fi
|
||||
[ ! -e "$WORK/sendico-credentials" ] || fail 'Invalid Sendico input mutated credentials'
|
||||
cat > "$WORK/sendico-input" <<'JSON'
|
||||
{"organization_ref":"000000000000000000000001","source_account_ref":"000000000000000000000002","client_id":"fixture.client","device_id":"fixture-device","login":"fixture@example.test","password":"fixture-password","client_secret":"fixture-secret"}
|
||||
JSON
|
||||
configure_sendico < "$WORK/sendico-input" >/dev/null
|
||||
jq -e 'keys==["client_secret","login","password"]' "$WORK/sendico-credentials" >/dev/null
|
||||
[ -f "$WORK/sendico-agents" ] || fail 'Sendico identity was not provisioned'
|
||||
if grep -Eq 'fixture-secret|fixture-password|fixture@example' "$STATE/sendico/settings.env" "$STATE/sendico/runtime.yml"; then fail 'Credential leaked into runtime configuration'; fi
|
||||
grep -q 'source_account_ref: 000000000000000000000002' "$STATE/sendico/runtime.yml"
|
||||
grep -q 'api_url: http://127.0.0.1:18081/api/v1' "$STATE/sendico/runtime.yml"
|
||||
echo 'PASS: Sendico operator input validation, secret separation and local runtime configuration'
|
||||
@@ -2,11 +2,16 @@ policies() (
|
||||
root_token=$VAULT_TOKEN
|
||||
identity=''
|
||||
trap 'if [ -n "$identity" ]; then printf "{}" | VAULT_TOKEN=$identity vault auth/token/revoke-self POST >/dev/null; fi' EXIT
|
||||
for service in bff notifications nats; do
|
||||
services='bff notifications nats'
|
||||
if [ -f "$STATE/sendico/settings.env" ]; then services="$services sendico"; fi
|
||||
for service in $services; do
|
||||
directory="$STATE/agents/$service"
|
||||
login=$(jq -n --rawfile role "$directory/role-id" --rawfile secret "$directory/secret-id" '{role_id:($role|rtrimstr("\n")),secret_id:($secret|rtrimstr("\n"))}' | vault auth/approle/login POST)
|
||||
identity=$(printf '%s' "$login" | jq -er '.auth.client_token')
|
||||
printf '%s' "$login" | jq '{token:.auth.client_token,paths:["kv/data/bootstrap/mongo","kv/data/infrastructure/mongo/bootstrap","sys/init","kv/data/bff","kv/data/caller","kv/data/services/notifications/callers/bff","kv/data/email","kv/data/nats","kv/data/tls"]}' | vault sys/capabilities POST | jq -e '.data | all(.[]; .==["deny"])' >/dev/null || fail 'Service can access bootstrap credentials'
|
||||
if [ "$service" = sendico ]; then
|
||||
printf '%s' "$login" | jq '{token:.auth.client_token,paths:["database/creds/bff","database/creds/notifications","kv/data/services/bff/jwt","kv/data/infrastructure/nats/auth"]}' | vault sys/capabilities POST | jq -e '.data | all(.[]; .==["deny"])' >/dev/null || fail 'Sendico session identity has unrelated secret access'
|
||||
fi
|
||||
VAULT_TOKEN=$(printf '%s' "$login" | jq -er '.auth.client_token')
|
||||
printf '{}' | vault auth/token/revoke-self POST >/dev/null
|
||||
VAULT_TOKEN=$root_token
|
||||
@@ -44,12 +49,18 @@ infrastructure() {
|
||||
# Resolve through Docker DNS while verifying the certificate for localhost.
|
||||
https() { curl -sS --max-time 5 --cacert "$STATE/ca.pem" --connect-to localhost:5126:frontend:8443 "$@"; }
|
||||
product() {
|
||||
compose ps --format json | jq -se 'length==6 and all(.[];.Health=="healthy")' >/dev/null || fail 'Expected six healthy services'
|
||||
expected=6
|
||||
if [ -f "$STATE/sendico/settings.env" ]; then expected=7; fi
|
||||
compose ps --format json | jq -se --argjson expected "$expected" 'length==$expected and all(.[];.Health=="healthy")' >/dev/null || fail "Expected $expected healthy services"
|
||||
https -f https://localhost:5126/ >/dev/null
|
||||
https -f https://localhost:5126/api/v1/health >/dev/null
|
||||
echo 'PASS: six healthy services, frontend and proxied BFF use verified TLS; no email sent'
|
||||
if [ "$expected" = 7 ]; then compose exec -T sendico sh /etc/sendico/check.sh; fi
|
||||
echo "PASS: $expected healthy services, frontend and proxied BFF use verified TLS; no email sent"
|
||||
}
|
||||
layout() {
|
||||
if [ -f "$STATE/sendico/settings.env" ]; then
|
||||
kv services/bff/sendico | jq -e 'keys==["client_secret","login","password"]' >/dev/null || fail 'Sendico secret contains non-secret configuration'
|
||||
fi
|
||||
kv services/notifications/email/resend | jq -e 'keys==["api_key"]' >/dev/null || fail 'Non-secret email configuration remains in Vault'
|
||||
for old in bootstrap/mongo bff caller services/notifications/callers/bff email nats tls frontend-tls notifications-tls; do
|
||||
[ "$(vault "kv/metadata/$old" GET yes)" = null ] || fail "Obsolete KV entry remains: $old"
|
||||
|
||||
Reference in new issue
Block a user