Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions client/src/generated/external.data.v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,8 @@ pub struct TvcApp {
#[serde(default)]
pub live_deployment_id: ::core::option::Option<::prost::alloc::string::String>,
pub public_domain: ::prost::alloc::string::String,
#[serde(default)]
pub enable_debug_mode_deployments: bool,
}
#[derive(Debug)]
#[derive(::serde::Serialize, ::serde::Deserialize)]
Expand Down
2 changes: 2 additions & 0 deletions client/src/generated/immutable.activity.v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2171,6 +2171,8 @@ pub struct CreateTvcAppIntent {
pub share_set_params: ::core::option::Option<TvcOperatorSetParams>,
#[serde(default)]
pub enable_egress: ::core::option::Option<bool>,
#[serde(default)]
pub enable_debug_mode_deployments: ::core::option::Option<bool>,
}
#[derive(Debug)]
#[derive(::serde::Serialize, ::serde::Deserialize)]
Expand Down
4 changes: 4 additions & 0 deletions proto/external/data/v1/tvc.proto
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ message TvcApp {
(google.api.field_behavior) = REQUIRED,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {description: "The public domain for ingress to this TVC App (in the format \"app-<ID>.turnkey.cloud\")."}
];
bool enable_debug_mode_deployments = 12 [
(google.api.field_behavior) = REQUIRED,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {description: "Whether this app permits debug-mode deployments. Set at app creation via CreateTvcAppIntent.enable_debug_mode_deployments and never updated thereafter. Debug-mode deployments expose logs and emit zero'd attestation PCRs, so remote attestation cannot succeed. The app's quorum key is therefore considered permanently insecure once enabled — a new app with a fresh quorum key must be created to return to a secure posture."}
];
}

message TvcDeployment {
Expand Down
4 changes: 4 additions & 0 deletions proto/immutable/activity/v1/activity.proto
Original file line number Diff line number Diff line change
Expand Up @@ -4153,6 +4153,10 @@ message CreateTvcAppIntent {
(google.api.field_behavior) = OPTIONAL,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {description: "Enables network egress for this TVC app. Default if not provided: false."}
];
optional bool enable_debug_mode_deployments = 8 [
(google.api.field_behavior) = OPTIONAL,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {description: "When true, this app may create deployments in debug-mode. Debug-mode deployments expose 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, and a new app with a fresh quorum key must be created. Default if not provided: false."}
];
}

message TvcOperatorSetParams {
Expand Down
49 changes: 48 additions & 1 deletion tvc/src/commands/app/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -140,9 +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: app_config.enable_debug_mode_deployments.into(),
}
}

fn apply_overrides(mut config: AppConfig, args: &Args) -> AppConfig {
config.enable_debug_mode_deployments = args.dangerous_enable_debug_mode_deployments;
config
}

fn to_tvc_operator_set_params(params: &OperatorSetParams) -> TvcOperatorSetParams {
TvcOperatorSetParams {
name: params.name.clone(),
Expand Down Expand Up @@ -181,6 +194,7 @@ mod tests {
}),
share_set_id: None,
share_set_params: None,
enable_debug_mode_deployments: false,
}
}

Expand Down Expand Up @@ -220,6 +234,39 @@ 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));
}
Comment thread
daniilrrr marked this conversation as resolved.

/// 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);
}

#[test]
fn build_intent_uses_share_set_id_when_configured() {
let mut config = valid_config();
Expand Down
60 changes: 47 additions & 13 deletions tvc/src/commands/deploy/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ 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 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.

Expand Down Expand Up @@ -90,10 +92,6 @@ pub struct Args {
)]
pub pivot_args: Vec<String>,

/// 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<u16>,
Expand All @@ -108,6 +106,21 @@ pub struct Args {
/// override `pivotContainerEncryptedPullSecret` from the config file.
#[arg(long, value_name = "PATH", env = "TVC_PIVOT_PULL_SECRET")]
pub pivot_pull_secret: Option<PathBuf>,

/// Deploy in debug mode, which forwards secure enclave logs to the host and
Comment thread
daniilrrr marked this conversation as resolved.
/// 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.
///
/// Overrides `debugMode` in the config file when set.
#[arg(long, env = "TVC_DANGEROUS_DEPLOY_DEBUG_MODE")]
pub dangerous_deploy_debug_mode: bool,
}

fn build_validate_image_request(
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -170,10 +183,7 @@ 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);
}
config.debug_mode = args.dangerous_deploy_debug_mode;
if let Some(v) = args.health_check_port {
config.health_check_port = v;
}
Expand Down Expand Up @@ -393,10 +403,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,
}
}

Expand All @@ -419,7 +429,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;
Expand Down Expand Up @@ -484,7 +494,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);
Expand Down Expand Up @@ -543,6 +553,30 @@ 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);
}
Comment on lines +559 to +568
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't need to change this, but this is why I don't like to hide logic inside functions. resolve_deploy_config Should just take Option<DeployConfig> as an argument which is the result of read_config_file. When we flatten out our logic, we don't have to do things like write an actual file and read it in just to see if the file that's read in is properly overwritten.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agree that this should be cleaned up and flattened into a pipeline of more pure functions. Won't do in this PR


/// 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.
Expand Down
6 changes: 6 additions & 0 deletions tvc/src/config/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ pub struct AppConfig {
pub share_set_id: Option<String>,
#[serde(default)]
pub share_set_params: Option<OperatorSetParams>,
/// 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)]
Expand Down Expand Up @@ -73,6 +78,7 @@ impl AppConfig {
}),
share_set_id: None,
share_set_params: None,
enable_debug_mode_deployments: false,
}
}

Expand Down
7 changes: 5 additions & 2 deletions tvc/src/config/deploy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,11 @@ pub struct DeployConfig {
#[serde(default)]
pub pivot_args: Vec<String>,
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<bool>,
pub debug_mode: bool,
#[serde(default)]
pub pivot_container_encrypted_pull_secret: Option<String>,
pub health_check_type: TvcHealthCheckType,
Expand All @@ -41,7 +44,7 @@ impl DeployConfig {
pivot_path: "<FILL_IN_PIVOT_PATH>".to_string(),
pivot_args: vec![],
expected_pivot_digest: "<FILL_IN_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,
Expand Down
Loading