diff --git a/tvc/src/cli.rs b/tvc/src/cli.rs index 0e1f407..366ebd1 100644 --- a/tvc/src/cli.rs +++ b/tvc/src/cli.rs @@ -14,8 +14,13 @@ Configuration values are resolved in this order, highest priority first: 3. Config file value (--config-file) 4. Built-in default -Special rules: +Special rules (exceptions to the order above): --pivot-args replaces the config file's list entirely (does not append) + Debug-mode flags (--dangerous-deploy-debug-mode and + --dangerous-enable-debug-mode-deployments) are opt-in only: the flag + or its env var can turn debug mode ON, but its absence never turns OFF + a config file that enables it. To disable debug mode, set it false in + the config file (or omit it) and do not pass the flag. Authentication: Local: run `tvc login` once; commands then read ~/.config/turnkey/. diff --git a/tvc/src/commands/app/create.rs b/tvc/src/commands/app/create.rs index 87e7673..30aea3f 100644 --- a/tvc/src/commands/app/create.rs +++ b/tvc/src/commands/app/create.rs @@ -17,11 +17,18 @@ pub struct Args { /// Path to the app configuration file (JSON). #[arg(short = 'c', long, value_name = "PATH", env = "TVC_APP_CONFIG")] pub config_file: PathBuf, + + /// Permit debug-mode deployments for this app. Debug-mode deployments expose + /// secure-enclave logs and emit zero'd attestation PCRs, so remote + /// attestation cannot succeed. Cannot be changed after app creation; setting + /// this true means the app's quorum key is considered permanently insecure. + #[arg(long, env = "TVC_DANGEROUS_ENABLE_DEBUG_MODE_DEPLOYMENTS")] + pub dangerous_enable_debug_mode_deployments: bool, } /// Run the app create command. pub async fn run(args: Args) -> Result<()> { - let app_config = load_or_fill_app_config(&args.config_file).await?; + let app_config = apply_overrides(load_or_fill_app_config(&args.config_file).await?, &args); app_config .validate() @@ -140,8 +147,15 @@ fn build_create_tvc_app_intent(app_config: &AppConfig) -> CreateTvcAppIntent { share_set_id: app_config.share_set_id.clone(), share_set_params: share_set_params.as_ref().map(to_tvc_operator_set_params), enable_egress: app_config.external_connectivity, - enable_debug_mode_deployments: None, + enable_debug_mode_deployments: app_config.enable_debug_mode_deployments.into(), + } +} + +fn apply_overrides(mut config: AppConfig, args: &Args) -> AppConfig { + if args.dangerous_enable_debug_mode_deployments { + config.enable_debug_mode_deployments = args.dangerous_enable_debug_mode_deployments; } + config } fn to_tvc_operator_set_params(params: &OperatorSetParams) -> TvcOperatorSetParams { @@ -182,6 +196,7 @@ mod tests { }), share_set_id: None, share_set_params: None, + enable_debug_mode_deployments: false, } } @@ -221,6 +236,55 @@ mod tests { ); } + /// Default config has debug-mode disabled, and the intent reports `false` + /// — explicit so the server doesn't have to fall back to a proto default. + #[test] + fn build_intent_sends_false_debug_mode_by_default() { + let intent = build_create_tvc_app_intent(&valid_config()); + assert_eq!(intent.enable_debug_mode_deployments, Some(false)); + } + + /// An explicit `enableDebugModeDeployments: true` in the config flows into + /// the intent so the server records the app's debug-mode capability. + #[test] + fn build_intent_forwards_debug_mode_from_config() { + let mut config = valid_config(); + config.enable_debug_mode_deployments = true; + + let intent = build_create_tvc_app_intent(&config); + assert_eq!(intent.enable_debug_mode_deployments, Some(true)); + } + + /// CLI flag flips a default `false` config to `true` — the user opted in + /// via the command line rather than the config file. + #[test] + fn dangerous_flag_enables_debug_mode_when_config_unset() { + let config = valid_config(); + let args = Args { + config_file: PathBuf::new(), + dangerous_enable_debug_mode_deployments: true, + }; + + let config = apply_overrides(config, &args); + assert!(config.enable_debug_mode_deployments); + } + + /// Omitting the CLI flag must NOT override a config that enables debug-mode + /// deployments: the flag is opt-in only and can never turn it off, so a + /// `true` config survives an absent flag. + #[test] + fn absent_dangerous_flag_preserves_config_debug_mode() { + let mut config = valid_config(); + config.enable_debug_mode_deployments = true; + let args = Args { + config_file: PathBuf::new(), + dangerous_enable_debug_mode_deployments: false, + }; + + let config = apply_overrides(config, &args); + assert!(config.enable_debug_mode_deployments); + } + #[test] fn build_intent_uses_share_set_id_when_configured() { let mut config = valid_config(); diff --git a/tvc/src/commands/deploy/create.rs b/tvc/src/commands/deploy/create.rs index 4b7a4f6..a2b1591 100644 --- a/tvc/src/commands/deploy/create.rs +++ b/tvc/src/commands/deploy/create.rs @@ -28,7 +28,11 @@ Required deployment fields: Special rules: --pivot-args replaces the config file's list entirely (does not append). - --debug-mode can enable debug mode but cannot disable a true config value. + --dangerous-deploy-debug-mode is opt-in only: the flag (or its env var) can + turn debug mode ON, but omitting it does not turn OFF debug mode enabled in + the config file. It also requires an app created with + `--dangerous-enable-debug-mode-deployments`; the server rejects debug-mode + deployments for apps without it. --pivot-pull-secret reads an unencrypted pull secret file, encrypts it for the active org's API environment, and overrides the encrypted secret in the config. @@ -90,10 +94,6 @@ pub struct Args { )] pub pivot_args: Vec, - /// Enable debug mode. One-way: cannot disable a `true` set earlier via the file. - #[arg(long, env = "TVC_DEBUG_MODE")] - pub debug_mode: bool, - /// Override the healthCheckPort field. #[arg(long, env = "TVC_HEALTH_CHECK_PORT")] pub health_check_port: Option, @@ -108,6 +108,19 @@ pub struct Args { /// override `pivotContainerEncryptedPullSecret` from the config file. #[arg(long, value_name = "PATH", env = "TVC_PIVOT_PULL_SECRET")] pub pivot_pull_secret: Option, + + /// Deploy in debug mode, which forwards secure enclave logs to the host and + /// zeroes attestation PCRs. This defeats the purpose of a secure enclave, + /// so it should only be used to debug non-prod applications and view application + /// logs. + /// + /// # WARNING + /// + /// Only valid for apps created with `--dangerous-enable-debug-mode-deployments`. + /// Debug-mode deployments permanently mark the app's quorum key as insecure; + /// to return to a secure posture, create a new app with a fresh quorum key. + #[arg(long, env = "TVC_DANGEROUS_DEPLOY_DEBUG_MODE")] + pub dangerous_deploy_debug_mode: bool, } fn build_validate_image_request( @@ -135,7 +148,7 @@ fn build_create_intent( pivot_args: deploy_config.pivot_args.clone(), expected_pivot_digest: deploy_config.expected_pivot_digest.clone(), pivot_container_encrypted_pull_secret, - debug_mode: deploy_config.debug_mode, + debug_mode: deploy_config.debug_mode.into(), nonce: None, health_check_type: deploy_config.health_check_type, health_check_port: deploy_config.health_check_port as u32, @@ -170,9 +183,8 @@ fn apply_overrides(config: &mut DeployConfig, args: &Args) { if !args.pivot_args.is_empty() { config.pivot_args = args.pivot_args.clone(); } - // One-way: only ever flips false -> true. - if args.debug_mode { - config.debug_mode = Some(true); + if args.dangerous_deploy_debug_mode { + config.debug_mode = args.dangerous_deploy_debug_mode; } if let Some(v) = args.health_check_port { config.health_check_port = v; @@ -393,10 +405,10 @@ mod tests { expected_pivot_digest: None, pivot_path: None, pivot_args: vec![], - debug_mode: false, health_check_port: None, public_ingress_port: None, pivot_pull_secret: None, + dangerous_deploy_debug_mode: false, } } @@ -419,7 +431,7 @@ mod tests { c.pivot_path = "file-path".into(); c.pivot_args = vec!["a".into(), "b".into()]; c.expected_pivot_digest = "file-digest".into(); - c.debug_mode = Some(false); + c.debug_mode = false; c.pivot_container_encrypted_pull_secret = None; c.health_check_port = 4000; c.public_ingress_port = 5000; @@ -484,7 +496,7 @@ mod tests { // Optional fields fall back to template defaults. assert_eq!(resolved.health_check_port, 3000); assert_eq!(resolved.public_ingress_port, 3000); - assert_eq!(resolved.debug_mode, Some(false)); + assert!(!resolved.debug_mode); assert!(resolved.pivot_args.is_empty()); // Pull-secret placeholder cleared in flag-only mode. assert_eq!(resolved.pivot_container_encrypted_pull_secret, None); @@ -543,6 +555,47 @@ mod tests { ); } + /// `--dangerous-deploy-debug-mode` flips the resolved `debug_mode` from + /// the file's `false` to `true`. + #[test] + fn dangerous_debug_mode_flag_enables_debug_mode() { + let file = write_config(&file_config()); // file has debug_mode = false + let args = Args { + config_file: Some(file.path().to_path_buf()), + dangerous_deploy_debug_mode: true, + ..empty_args() + }; + let resolved = run_resolve(&args).unwrap(); + assert!(resolved.debug_mode); + } + + /// Omitting `--dangerous-deploy-debug-mode` must NOT override a config file + /// that enables debug mode: the flag is opt-in only and can never turn it + /// off, so a `debug_mode = true` config survives an absent flag. + #[test] + fn absent_debug_mode_flag_preserves_config_debug_mode() { + let mut cfg = file_config(); + cfg.debug_mode = true; + let file = write_config(&cfg); + let args = Args { + config_file: Some(file.path().to_path_buf()), + dangerous_deploy_debug_mode: false, + ..empty_args() + }; + let resolved = run_resolve(&args).unwrap(); + assert!(resolved.debug_mode); + } + + /// The intent builder wraps the resolved config's debug_mode in `Some(...)` + /// when constructing the outgoing `CreateTvcDeploymentIntent`. + #[test] + fn build_intent_forwards_debug_mode() { + let mut cfg = file_config(); + cfg.debug_mode = true; + let intent = build_create_intent(&cfg, "image-url".to_string(), None); + assert_eq!(intent.debug_mode, Some(true)); + } + /// Sets `TVC_NON_INTERACTIVE=1` for the lifetime of the value so a test /// can exercise the "non-interactive bails with flag list" branch /// regardless of how the test runner is invoked. diff --git a/tvc/src/config/app.rs b/tvc/src/config/app.rs index 091e56e..8d2db63 100644 --- a/tvc/src/config/app.rs +++ b/tvc/src/config/app.rs @@ -22,6 +22,11 @@ pub struct AppConfig { pub share_set_id: Option, #[serde(default)] pub share_set_params: Option, + /// Whether this app permits debug-mode deployments. Must be set at app + /// creation and cannot be changed after. Setting this true means the app's + /// quorum key is considered permanently insecure. + #[serde(default)] + pub enable_debug_mode_deployments: bool, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -73,6 +78,7 @@ impl AppConfig { }), share_set_id: None, share_set_params: None, + enable_debug_mode_deployments: false, } } diff --git a/tvc/src/config/deploy.rs b/tvc/src/config/deploy.rs index d73da8b..0f5f48d 100644 --- a/tvc/src/config/deploy.rs +++ b/tvc/src/config/deploy.rs @@ -21,8 +21,11 @@ pub struct DeployConfig { #[serde(default)] pub pivot_args: Vec, pub expected_pivot_digest: String, + /// Deploy in debug mode. A deployment with debug enabled is permanently + /// marked insecure: enclave logs are forwarded to the host and attestation + /// PCRs are zeroed, so anything the enclave processed may have leaked. #[serde(default)] - pub debug_mode: Option, + pub debug_mode: bool, #[serde(default)] pub pivot_container_encrypted_pull_secret: Option, pub health_check_type: TvcHealthCheckType, @@ -41,7 +44,7 @@ impl DeployConfig { pivot_path: "".to_string(), pivot_args: vec![], expected_pivot_digest: "".to_string(), - debug_mode: Some(false), + debug_mode: false, pivot_container_encrypted_pull_secret: Some(PULL_SECRET_PLACEHOLDER.to_string()), health_check_type: TvcHealthCheckType::Http, health_check_port: 3000,