2626
2727DOCKERFILE_NAME = "Dockerfile"
2828
29+ # Environment variables passed to the deploy script.
30+ ENV_GCP_PROJECT = "GCP_PROJECT"
31+ ENV_GCP_REGION = "GCP_REGION"
32+ ENV_SERVICE_NAME = "SERVICE_NAME"
33+ ENV_AR_REPO = "AR_REPO"
34+ ENV_VERSION = "VERSION"
35+
36+ # Manifest response field names from flexgen.
37+ FIELD_DOCKERFILE = "dockerfile"
38+ FIELD_DEPLOY_COMMAND = "deploy_command"
39+
40+ # Allowlist of host environment variables forwarded to the deploy script.
41+ # We deliberately exclude things like AWS_*/GITHUB_TOKEN/SSH agent sockets so a
42+ # compromised or tampered manifest cannot exfiltrate unrelated credentials.
43+ DEPLOY_ENV_ALLOWLIST = frozenset ({
44+ "PATH" ,
45+ "HOME" ,
46+ "USER" ,
47+ "LOGNAME" ,
48+ "SHELL" ,
49+ "TERM" ,
50+ "LANG" ,
51+ "LC_ALL" ,
52+ "LC_CTYPE" ,
53+ "TMPDIR" ,
54+ "TEMP" ,
55+ "TMP" ,
56+ "XDG_CONFIG_HOME" ,
57+ # gcloud configuration
58+ "CLOUDSDK_CONFIG" ,
59+ "CLOUDSDK_ACTIVE_CONFIG_NAME" ,
60+ "CLOUDSDK_CORE_PROJECT" ,
61+ "CLOUDSDK_CORE_ACCOUNT" ,
62+ "CLOUDSDK_AUTH_ACCESS_TOKEN_FILE" ,
63+ "GOOGLE_APPLICATION_CREDENTIALS" ,
64+ # docker configuration
65+ "DOCKER_HOST" ,
66+ "DOCKER_TLS_VERIFY" ,
67+ "DOCKER_CERT_PATH" ,
68+ "DOCKER_CONFIG" ,
69+ "DOCKER_BUILDKIT" ,
70+ # corporate proxy / TLS trust
71+ "HTTP_PROXY" ,
72+ "HTTPS_PROXY" ,
73+ "NO_PROXY" ,
74+ "http_proxy" ,
75+ "https_proxy" ,
76+ "no_proxy" ,
77+ "SSL_CERT_FILE" ,
78+ "SSL_CERT_DIR" ,
79+ "REQUESTS_CA_BUNDLE" ,
80+ "CURL_CA_BUNDLE" ,
81+ })
82+
2983
3084@click .group ()
3185def gcp_cli ():
@@ -78,6 +132,12 @@ def gcp_cli():
78132 help = "Overwrite an existing Dockerfile without prompting." ,
79133)
80134@click .option ("--token" , help = "The Reflex authentication token." )
135+ @click .option (
136+ "--interactive/--no-interactive" ,
137+ is_flag = True ,
138+ default = True ,
139+ help = "Whether to prompt before overwriting the Dockerfile and running the script." ,
140+ )
81141@click .option (
82142 "--dry-run" ,
83143 is_flag = True ,
@@ -99,6 +159,7 @@ def gcp_deploy(
99159 source_dir : str ,
100160 overwrite_dockerfile : bool ,
101161 token : str | None ,
162+ interactive : bool ,
102163 dry_run : bool ,
103164 loglevel : str ,
104165):
@@ -112,7 +173,7 @@ def gcp_deploy(
112173 console .set_log_level (loglevel )
113174
114175 authenticated_client = hosting .get_authenticated_client (
115- token = token , interactive = True
176+ token = token , interactive = interactive
116177 )
117178
118179 bash_path = shutil .which ("bash" )
@@ -154,11 +215,11 @@ def gcp_deploy(
154215
155216 version_value = version_tag or datetime .now (timezone .utc ).strftime ("%Y%m%d-%H%M%S" )
156217 deploy_env = {
157- "GCP_PROJECT" : gcp_project ,
158- "GCP_REGION" : region ,
159- "SERVICE_NAME" : service_name ,
160- "AR_REPO" : ar_repo ,
161- "VERSION" : version_value ,
218+ ENV_GCP_PROJECT : gcp_project ,
219+ ENV_GCP_REGION : region ,
220+ ENV_SERVICE_NAME : service_name ,
221+ ENV_AR_REPO : ar_repo ,
222+ ENV_VERSION : version_value ,
162223 }
163224
164225 console .info ("Received deploy manifest from flexgen." )
@@ -172,6 +233,10 @@ def gcp_deploy(
172233 console .print ("─" * 60 )
173234 console .print (deploy_script )
174235 console .print ("─" * 60 )
236+ console .info (
237+ f"The script runs with a restricted env (only { len (DEPLOY_ENV_ALLOWLIST )} "
238+ "allowlisted host variables forwarded plus the deploy variables above)."
239+ )
175240
176241 if dry_run :
177242 console .print ("" )
@@ -182,13 +247,20 @@ def gcp_deploy(
182247 console .info ("Dry run — nothing written or executed." )
183248 return
184249
185- if not _write_dockerfile (dockerfile_path , dockerfile , overwrite_dockerfile ):
250+ if not _write_dockerfile (
251+ dockerfile_path , dockerfile , overwrite_dockerfile , interactive
252+ ):
186253 raise click .exceptions .Exit (1 )
187254
188- answer = console .ask ("Run the deploy script now?" , choices = ["y" , "n" ], default = "y" )
189- if answer != "y" :
190- console .warn ("Aborted by user. The Dockerfile has been written for later use." )
191- raise click .exceptions .Exit (1 )
255+ if interactive :
256+ answer = console .ask (
257+ "Run the deploy script now?" , choices = ["y" , "n" ], default = "y"
258+ )
259+ if answer != "y" :
260+ console .warn (
261+ "Aborted by user. The Dockerfile has been written for later use."
262+ )
263+ raise click .exceptions .Exit (1 )
192264
193265 exit_code = _run_deploy_script (
194266 bash_path = bash_path ,
@@ -284,31 +356,44 @@ def _request_manifest(token: str) -> tuple[str, str]:
284356 console .error ("Flexgen returned an unexpected response shape." )
285357 raise click .exceptions .Exit (1 )
286358
287- dockerfile = body .get ("dockerfile" )
288- deploy_command = body .get ("deploy_command" )
359+ dockerfile = body .get (FIELD_DOCKERFILE )
360+ deploy_command = body .get (FIELD_DEPLOY_COMMAND )
289361 if not isinstance (dockerfile , str ) or not dockerfile .strip ():
290- console .error ("Flexgen response is missing a non-empty 'dockerfile' field." )
362+ console .error (
363+ f"Flexgen response is missing a non-empty { FIELD_DOCKERFILE !r} field."
364+ )
291365 raise click .exceptions .Exit (1 )
292366 if not isinstance (deploy_command , str ) or not deploy_command .strip ():
293- console .error ("Flexgen response is missing a non-empty 'deploy_command' field." )
367+ console .error (
368+ f"Flexgen response is missing a non-empty { FIELD_DEPLOY_COMMAND !r} field."
369+ )
294370 raise click .exceptions .Exit (1 )
295371
296372 return dockerfile , deploy_command
297373
298374
299- def _write_dockerfile (path : Path , contents : str , overwrite : bool ) -> bool :
300- """Write the Dockerfile to disk, prompting before overwriting.
375+ def _write_dockerfile (
376+ path : Path , contents : str , overwrite : bool , interactive : bool
377+ ) -> bool :
378+ """Write the Dockerfile to disk, prompting before overwriting in interactive mode.
301379
302380 Args:
303381 path: Where to write the Dockerfile.
304382 contents: The Dockerfile body.
305383 overwrite: If True, overwrite without prompting.
384+ interactive: If False, never prompt; require `overwrite` when the file exists.
306385
307386 Returns:
308387 True on success, False if the user declined to overwrite or write failed.
309388
310389 """
311390 if path .exists () and not overwrite :
391+ if not interactive :
392+ console .error (
393+ f"{ path } already exists. Pass --overwrite-dockerfile to replace it "
394+ "in non-interactive mode."
395+ )
396+ return False
312397 answer = console .ask (
313398 f"{ path } already exists. Overwrite?" , choices = ["y" , "n" ], default = "n"
314399 )
@@ -335,17 +420,25 @@ def _run_deploy_script(
335420) -> int :
336421 """Run the bash deploy script, streaming output to the user's terminal.
337422
423+ The script's environment is restricted to ``DEPLOY_ENV_ALLOWLIST`` (plus the
424+ explicit ``env_overrides``) so unrelated host secrets like ``AWS_*`` or
425+ ``GITHUB_TOKEN`` cannot be exfiltrated by a tampered or compromised manifest.
426+
338427 Args:
339428 bash_path: Resolved path to the bash executable.
340429 script: The bash script body received from flexgen.
341430 cwd: Working directory to run the script in.
342- env_overrides: Environment variables to layer on top of the parent env .
431+ env_overrides: Environment variables required by the deploy script .
343432
344433 Returns:
345434 The exit code of the bash process.
346435
347436 """
348- env = os .environ .copy ()
437+ env = {
438+ name : value
439+ for name , value in os .environ .items ()
440+ if name in DEPLOY_ENV_ALLOWLIST
441+ }
349442 env .update (env_overrides )
350443 try :
351444 result = subprocess .run (
0 commit comments