Skip to content

Commit 86fa3c0

Browse files
Update gen_secrets.sh
1 parent 07a43ce commit 86fa3c0

1 file changed

Lines changed: 211 additions & 18 deletions

File tree

gen_secrets.sh

Lines changed: 211 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,26 @@
66
#
77
# gen_secrets.sh
88
#
9-
# Generates a .env file by resolving secret references in a env.config file.
9+
# Generates a .env file by resolving secret references in an env.config file.
1010
#
1111
# env.config format:
12-
# aws_profile=<profile> # consumed by this script, not written to dest
13-
# KEY=@aws_secret_name # fetched via ih-secrets at run time
14-
# KEY=@aws_secret_name:key # fetched and extracted from JSON response
15-
# KEY=~hex:32 # locally generated via openssl rand -hex <n>
16-
# KEY=~base64:32 # locally generated via openssl rand -base64 <n>
17-
# KEY=value # written verbatim
18-
# # comment / blank lines # written verbatim
12+
# aws_profile=<profile> # ih-secrets profile, not written
13+
# aws_profile=1p # beta: fetch @ references from Dev Secrets
14+
# INTERNAL_TOOLS_API_URL=<url> # written verbatim; not used for fetches
15+
# KEY=@aws_secret_name # fetched via ih-secrets at run time
16+
# KEY=@aws_secret_name:key # fetched and extracted from JSON response
17+
# KEY=~hex:32 # locally generated via openssl rand -hex <n>
18+
# KEY=~base64:32 # locally generated via openssl rand -base64 <n>
19+
# KEY=value # written verbatim
20+
# # comment / blank lines # written verbatim
21+
#
22+
# In aws_profile=1p mode, secret fetches call the Internal Tools backend
23+
# directly using a short-lived AWS STS presigned URL. That auth uses the active
24+
# AWS credential chain; set AWS_PROFILE or GEN_SECRETS_STS_AWS_PROFILE if your
25+
# default AWS credentials are not the TinyFish SSO session you want.
26+
#
27+
# Default backend URL is https://api.internal-tools.production.tinyfish.io.
28+
# For local backend testing, override with GEN_SECRETS_API_URL or DEV_SECRETS_API_URL.
1929
#
2030
# Default behavior (fill mode): if the dest file already exists, keys that
2131
# already have a non-empty value are kept as-is. Only absent or empty keys
@@ -31,6 +41,7 @@ CONFIG="env.config"
3141
DEST=".env.local"
3242
FORCE=0
3343
REGEN=0
44+
DEFAULT_BACKEND_URL="https://api.internal-tools.production.tinyfish.io"
3445

3546
while [[ $# -gt 0 ]]; do
3647
case "$1" in
@@ -56,19 +67,52 @@ if [[ ! -f "$CONFIG" ]]; then
5667
fi
5768

5869
# First pass: extract aws_profile
59-
AWS_PROFILE=""
70+
CONFIG_AWS_PROFILE=""
6071
while IFS= read -r line || [[ -n "$line" ]]; do
61-
if [[ "$line" =~ ^aws_profile=(.+)$ ]]; then
62-
AWS_PROFILE="${BASH_REMATCH[1]}"
63-
break
64-
fi
72+
case "$line" in
73+
aws_profile=*)
74+
CONFIG_AWS_PROFILE="${line#aws_profile=}"
75+
;;
76+
esac
6577
done < "$CONFIG"
6678

67-
if [[ -z "$AWS_PROFILE" ]]; then
79+
if [[ -z "$CONFIG_AWS_PROFILE" ]]; then
6880
echo "ERROR: 'aws_profile' not set in $CONFIG" >&2
6981
exit 1
7082
fi
7183

84+
USE_DEV_SECRETS=0
85+
if [[ "$CONFIG_AWS_PROFILE" == "1p" ]]; then
86+
USE_DEV_SECRETS=1
87+
fi
88+
89+
validate_backend_url() {
90+
python3 - "$1" <<'PY'
91+
import sys
92+
from urllib.parse import urlparse
93+
94+
url = sys.argv[1]
95+
parsed = urlparse(url)
96+
if not parsed.scheme or not parsed.netloc:
97+
print("ERROR: Dev Secrets backend URL must be an absolute URL", file=sys.stderr)
98+
sys.exit(1)
99+
is_local_http = parsed.scheme == "http" and parsed.hostname in {"localhost", "127.0.0.1", "::1"}
100+
if parsed.scheme == "http" and not is_local_http:
101+
print("ERROR: Dev Secrets backend URL must use https unless it points at localhost", file=sys.stderr)
102+
sys.exit(1)
103+
if parsed.scheme not in {"http", "https"}:
104+
print("ERROR: Dev Secrets backend URL must use http or https", file=sys.stderr)
105+
sys.exit(1)
106+
PY
107+
}
108+
109+
BACKEND_URL=""
110+
if [[ "$USE_DEV_SECRETS" -eq 1 ]]; then
111+
BACKEND_URL="${GEN_SECRETS_API_URL:-${DEV_SECRETS_API_URL:-$DEFAULT_BACKEND_URL}}"
112+
BACKEND_URL="${BACKEND_URL%/}"
113+
validate_backend_url "$BACKEND_URL"
114+
fi
115+
72116
# In fill mode, check if dest exists to decide whether we're filling or generating fresh
73117
FILL=0
74118
if [[ -f "$DEST" && "$REGEN" -eq 0 ]]; then
@@ -106,6 +150,138 @@ _key_processed() {
106150
return 1
107151
}
108152

153+
urlencode() {
154+
python3 - "$1" <<'PY'
155+
import sys
156+
from urllib.parse import quote
157+
158+
print(quote(sys.argv[1], safe=""))
159+
PY
160+
}
161+
162+
print_aws_sso_hint() {
163+
if [[ -n "${GEN_SECRETS_STS_AWS_PROFILE:-}" ]]; then
164+
echo "Run \`aws sso login --profile $GEN_SECRETS_STS_AWS_PROFILE\` and try again." >&2
165+
else
166+
echo "Run \`aws sso login\` and try again. If you use a non-default profile, set GEN_SECRETS_STS_AWS_PROFILE=<profile>." >&2
167+
fi
168+
}
169+
170+
ensure_sts_auth_token() {
171+
local now
172+
if [[ -n "${STS_AUTH_TOKEN:-}" && "${STS_AUTH_TOKEN_CREATED_AT:-}" =~ ^[0-9]+$ ]]; then
173+
now=$(date +%s)
174+
if (( now - STS_AUTH_TOKEN_CREATED_AT < 55 )); then
175+
return
176+
fi
177+
fi
178+
179+
local credentials_json
180+
if [[ -n "${GEN_SECRETS_STS_AWS_PROFILE:-}" ]]; then
181+
credentials_json=$(aws configure export-credentials --profile "$GEN_SECRETS_STS_AWS_PROFILE" --format process 2>/dev/null) || {
182+
echo "ERROR: Could not export AWS credentials for Dev Secrets auth." >&2
183+
print_aws_sso_hint
184+
exit 1
185+
}
186+
elif ! credentials_json=$(aws configure export-credentials --format process 2>/dev/null); then
187+
echo "ERROR: Could not export AWS credentials for Dev Secrets auth." >&2
188+
print_aws_sso_hint
189+
exit 1
190+
fi
191+
192+
if ! STS_AUTH_TOKEN=$(AWS_CREDENTIALS_JSON="$credentials_json" python3 - <<'PY'
193+
import base64
194+
import datetime as dt
195+
import hashlib
196+
import hmac
197+
import json
198+
import os
199+
import sys
200+
from urllib.parse import quote
201+
202+
try:
203+
creds = json.loads(os.environ["AWS_CREDENTIALS_JSON"])
204+
access_key = creds["AccessKeyId"]
205+
secret_key = creds["SecretAccessKey"]
206+
session_token = creds.get("SessionToken")
207+
except Exception as exc:
208+
print(f"ERROR: Could not parse exported AWS credentials: {exc}", file=sys.stderr)
209+
sys.exit(1)
210+
211+
region = "us-east-1"
212+
service = "sts"
213+
host = "sts.amazonaws.com"
214+
now = dt.datetime.now(dt.timezone.utc)
215+
amz_date = now.strftime("%Y%m%dT%H%M%SZ")
216+
date_stamp = now.strftime("%Y%m%d")
217+
scope = f"{date_stamp}/{region}/{service}/aws4_request"
218+
219+
params = {
220+
"Action": "GetCallerIdentity",
221+
"Version": "2011-06-15",
222+
"X-Amz-Algorithm": "AWS4-HMAC-SHA256",
223+
"X-Amz-Credential": f"{access_key}/{scope}",
224+
"X-Amz-Date": amz_date,
225+
"X-Amz-Expires": "60",
226+
"X-Amz-SignedHeaders": "host",
227+
}
228+
if session_token:
229+
params["X-Amz-Security-Token"] = session_token
230+
231+
def q(value: str) -> str:
232+
return quote(str(value), safe="-_.~")
233+
234+
canonical_query = "&".join(f"{q(k)}={q(v)}" for k, v in sorted(params.items()))
235+
payload_hash = hashlib.sha256(b"").hexdigest()
236+
canonical_request = f"GET\n/\n{canonical_query}\nhost:{host}\n\nhost\n{payload_hash}"
237+
string_to_sign = "\n".join(
238+
[
239+
"AWS4-HMAC-SHA256",
240+
amz_date,
241+
scope,
242+
hashlib.sha256(canonical_request.encode()).hexdigest(),
243+
]
244+
)
245+
246+
def sign(key: bytes, msg: str) -> bytes:
247+
return hmac.new(key, msg.encode(), hashlib.sha256).digest()
248+
249+
signing_key = sign(sign(sign(sign(("AWS4" + secret_key).encode(), date_stamp), region), service), "aws4_request")
250+
signature = hmac.new(signing_key, string_to_sign.encode(), hashlib.sha256).hexdigest()
251+
presigned_url = f"https://{host}/?{canonical_query}&X-Amz-Signature={signature}"
252+
print(base64.b64encode(presigned_url.encode()).decode())
253+
PY
254+
); then
255+
exit 1
256+
fi
257+
STS_AUTH_TOKEN_CREATED_AT=$(date +%s)
258+
}
259+
260+
fetch_dev_secret() {
261+
local secret="$1"
262+
local encoded_secret
263+
encoded_secret=$(urlencode "$secret")
264+
ensure_sts_auth_token
265+
curl -fsS \
266+
--connect-timeout "${GEN_SECRETS_CONNECT_TIMEOUT_SECONDS:-5}" \
267+
--max-time "${GEN_SECRETS_FETCH_TIMEOUT_SECONDS:-30}" \
268+
-H "Authorization: AWS-STS $STS_AUTH_TOKEN" \
269+
"$BACKEND_URL/secrets/credential/$encoded_secret"
270+
}
271+
272+
fetch_secret() {
273+
local secret="$1"
274+
if [[ "$USE_DEV_SECRETS" -eq 1 ]]; then
275+
fetch_dev_secret "$secret"
276+
else
277+
ih-secrets --aws-profile "$CONFIG_AWS_PROFILE" get "$secret"
278+
fi
279+
}
280+
281+
if [[ "$USE_DEV_SECRETS" -eq 1 ]]; then
282+
ensure_sts_auth_token
283+
fi
284+
109285
TMPFILE=$(mktemp)
110286
trap 'rm -f "$TMPFILE"' EXIT
111287

@@ -117,9 +293,17 @@ FAILED_KEYS=()
117293
PROCESSED_KEYS=()
118294

119295
if [[ "$FILL" -eq 1 ]]; then
120-
echo "Filling $DEST from $CONFIG (profile: $AWS_PROFILE) ..."
296+
if [[ "$USE_DEV_SECRETS" -eq 1 ]]; then
297+
echo "Filling $DEST from $CONFIG (profile: $CONFIG_AWS_PROFILE, backend: $BACKEND_URL) ..."
298+
else
299+
echo "Filling $DEST from $CONFIG (profile: $CONFIG_AWS_PROFILE) ..."
300+
fi
121301
else
122-
echo "Generating $DEST from $CONFIG (profile: $AWS_PROFILE) ..."
302+
if [[ "$USE_DEV_SECRETS" -eq 1 ]]; then
303+
echo "Generating $DEST from $CONFIG (profile: $CONFIG_AWS_PROFILE, backend: $BACKEND_URL) ..."
304+
else
305+
echo "Generating $DEST from $CONFIG (profile: $CONFIG_AWS_PROFILE) ..."
306+
fi
123307
fi
124308
echo ""
125309

@@ -154,7 +338,7 @@ while IFS= read -r line || [[ -n "$line" ]]; do
154338
secret="${BASH_REMATCH[2]}"
155339
json_key="${BASH_REMATCH[4]}"
156340
printf " [FETCH] %-45s ... " "$key"
157-
if raw=$(ih-secrets --aws-profile "$AWS_PROFILE" get "$secret" 2>&1); then
341+
if raw=$(fetch_secret "$secret" 2>&1); then
158342
if [[ -n "$json_key" ]]; then
159343
if ! value=$(python3 -c "import json,sys; print(json.loads(sys.argv[1])[sys.argv[2]])" "$raw" "$json_key" 2>&1); then
160344
printf '%s=\n' "$key" >> "$TMPFILE"
@@ -172,6 +356,11 @@ while IFS= read -r line || [[ -n "$line" ]]; do
172356
else
173357
printf '%s=\n' "$key" >> "$TMPFILE"
174358
echo "FAILED"
359+
if [[ -n "$raw" ]]; then
360+
while IFS= read -r err_line || [[ -n "$err_line" ]]; do
361+
[[ -n "$err_line" ]] && printf ' %s\n' "$err_line"
362+
done <<< "$raw"
363+
fi
175364
FAILED=$((FAILED + 1))
176365
FAILED_KEYS+=("$key ($secret)")
177366
fi
@@ -233,6 +422,10 @@ if [[ "$FAILED" -gt 0 ]]; then
233422
for k in "${FAILED_KEYS[@]}"; do
234423
echo " - $k"
235424
done
236-
echo "Check your access to AWS profile '$AWS_PROFILE'."
425+
if [[ "$USE_DEV_SECRETS" -eq 1 ]]; then
426+
echo "Check your Dev Secrets backend access at '$BACKEND_URL'."
427+
else
428+
echo "Check your ih-secrets access for profile '$CONFIG_AWS_PROFILE'."
429+
fi
237430
exit 1
238431
fi

0 commit comments

Comments
 (0)