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"
3141DEST=" .env.local"
3242FORCE=0
3343REGEN=0
44+ DEFAULT_BACKEND_URL=" https://api.internal-tools.production.tinyfish.io"
3445
3546while [[ $# -gt 0 ]]; do
3647 case " $1 " in
@@ -56,19 +67,52 @@ if [[ ! -f "$CONFIG" ]]; then
5667fi
5768
5869# First pass: extract aws_profile
59- AWS_PROFILE =" "
70+ CONFIG_AWS_PROFILE =" "
6071while 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
6577done < " $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
7082fi
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
73117FILL=0
74118if [[ -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+
109285TMPFILE=$( mktemp)
110286trap ' rm -f "$TMPFILE"' EXIT
111287
@@ -117,9 +293,17 @@ FAILED_KEYS=()
117293PROCESSED_KEYS=()
118294
119295if [[ " $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
121301else
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
123307fi
124308echo " "
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
238431fi
0 commit comments