Context
AppRole (src/bootroot_cmd.rs) holds the credential pair bootroot mints: role_id and secret_id. It derives Debug, so {:?} of an AppRole, or of anything that derives Debug over one, prints both values in clear text.
No deploy-core code formats an AppRole today. The hazard is downstream. bootler holds AppRoles in types that derive Debug. For example, RegistrationSteps (core/src/services.rs) derives Debug and holds service_add: Option<&AppRole>. A future tracing field, expect message or error string over any of them would print the secret id. bootler#617 redacted the records it owns, and it had to print the AppRole as "<redacted>" by hand because this type's own Debug leaks. Fixing the type here fixes every container at once.
deploy-core already redacts in the same way elsewhere: GenerationFile (src/generation.rs) has a hand-written Debug.
This is deploy-core's first release line (0.1.0, 2026-10-02). No compatibility shim is needed.
Scope
In src/bootroot_cmd.rs only:
- Remove
Debug from AppRole's derive list. Keep Clone, PartialEq, Eq, Serialize and Deserialize, and every field, its type and its visibility, as they are.
- Add a hand-written
impl std::fmt::Debug for AppRole that uses f.debug_struct("AppRole"), lists role_id and then secret_id, prints each as the &str "<redacted>", and ends with .finish(). Give it a one-line comment saying why it is hand-written.
- Add a
#[cfg(test)] mod tests to the file with the test in the Test plan.
In CHANGELOG.md, open an ## [Unreleased] section above ## [0.1.0] with a ### Changed entry: an AppRole's Debug output no longer shows its role id or secret id, and both print as <redacted>. Add the matching link reference [Unreleased]: https://github.com/aicers/deploy-core/compare/0.1.0...main above the [0.1.0] reference. A user of 0.1.0 can observe this change, so it gets an entry. The entry carries no issue or PR reference.
Acceptance criteria
Constraints
- Hand-written
Debug only: no wrapper type, newtype, helper, macro or dependency, and no change to field types or visibility.
- Use exactly the
"<redacted>" literal and .finish(), not .finish_non_exhaustive(), so a field added later must be classified explicitly.
- Never format a secret through its own
Debug inside the impl.
- Never compare secret values with
assert_eq! or assert_ne!, which print both sides through Debug on failure. Use assert!(a == b).
- First version: add no compatibility shim or feature flag.
- Cut no release. Releasing is not this issue's.
Edge cases
role_id. It is redacted too. It is one half of the AppRole login, and bootler's own redactions (#617's SecretsRecord, BootstrapMaterial) already treat the whole pair as secret.
- Pretty-printing.
{:#?} goes through the same debug_struct and redacts the same way. The test covers both.
Test plan
Dependencies
- None blocking.
- bootler's issue "Redact Debug for bootroot's init capture" (aicers/bootler#NNN, filled in at filing) re-pins deploy-core to this issue's merge commit, so this merges first.
Out of scope
service_add_args passing the secret id on bootroot service add's argv. Its rustdoc already records that as an accepted trade-off.
- Any other deploy-core type's
Debug.
- Zeroizing memory, or a secret wrapper type.
Pointers
src/bootroot_cmd.rs (AppRole); src/registration.rs (ServiceAddSpec::approle, service_add_args).
src/generation.rs (GenerationFile's hand-written Debug, the in-crate precedent).
- bootler:
core/src/secrets.rs (SecretsRecord's Debug, which prints the AppRole as "<redacted>"); core/src/services.rs (RegistrationSteps).
Context
AppRole(src/bootroot_cmd.rs) holds the credential pair bootroot mints:role_idandsecret_id. It derivesDebug, so{:?}of anAppRole, or of anything that derivesDebugover one, prints both values in clear text.No deploy-core code formats an
AppRoletoday. The hazard is downstream. bootler holdsAppRoles in types that deriveDebug. For example,RegistrationSteps(core/src/services.rs) derivesDebugand holdsservice_add: Option<&AppRole>. A futuretracingfield,expectmessage or error string over any of them would print the secret id. bootler#617 redacted the records it owns, and it had to print theAppRoleas"<redacted>"by hand because this type's ownDebugleaks. Fixing the type here fixes every container at once.deploy-core already redacts in the same way elsewhere:
GenerationFile(src/generation.rs) has a hand-writtenDebug.This is deploy-core's first release line (
0.1.0, 2026-10-02). No compatibility shim is needed.Scope
In
src/bootroot_cmd.rsonly:DebugfromAppRole'sderivelist. KeepClone,PartialEq,Eq,SerializeandDeserialize, and every field, its type and its visibility, as they are.impl std::fmt::Debug for AppRolethat usesf.debug_struct("AppRole"), listsrole_idand thensecret_id, prints each as the&str"<redacted>", and ends with.finish(). Give it a one-line comment saying why it is hand-written.#[cfg(test)] mod teststo the file with the test in the Test plan.In
CHANGELOG.md, open an## [Unreleased]section above## [0.1.0]with a### Changedentry: anAppRole'sDebugoutput no longer shows its role id or secret id, and both print as<redacted>. Add the matching link reference[Unreleased]: https://github.com/aicers/deploy-core/compare/0.1.0...mainabove the[0.1.0]reference. A user of0.1.0can observe this change, so it gets an entry. The entry carries no issue or PR reference.Acceptance criteria
AppRoleno longer derivesDebug, and its hand-writtenDebugprintsAppRole { role_id: "<redacted>", secret_id: "<redacted>" }.format!("{approle:?}")andformat!("{approle:#?}")contain neither sentinel, and do contain<redacted>and the field namesrole_idandsecret_id.CHANGELOG.mdentry and link reference are present, and the Markdown job passes.test-support, Docs, Test and Test withtest-support, and the Platform jobs.Constraints
Debugonly: no wrapper type, newtype, helper, macro or dependency, and no change to field types or visibility."<redacted>"literal and.finish(), not.finish_non_exhaustive(), so a field added later must be classified explicitly.Debuginside the impl.assert_eq!orassert_ne!, which print both sides throughDebugon failure. Useassert!(a == b).Edge cases
role_id. It is redacted too. It is one half of the AppRole login, and bootler's own redactions (#617'sSecretsRecord,BootstrapMaterial) already treat the whole pair as secret.{:#?}goes through the samedebug_structand redacts the same way. The test covers both.Test plan
app_role_debug_redacts_both_ids: build anAppRolewith a distinct sentinel inrole_idand insecret_id. Assert thatformat!("{approle:?}")andformat!("{approle:#?}")contain neither sentinel, and contain<redacted>,role_idandsecret_id.cargo fmt --check,cargo clippy --all-targets -- -D warningswith and without--features test-support,cargo docas the Docs job runs it, andcargo testwith and without--features test-support.Dependencies
Out of scope
service_add_argspassing the secret id onbootroot service add's argv. Its rustdoc already records that as an accepted trade-off.Debug.Pointers
src/bootroot_cmd.rs(AppRole);src/registration.rs(ServiceAddSpec::approle,service_add_args).src/generation.rs(GenerationFile's hand-writtenDebug, the in-crate precedent).core/src/secrets.rs(SecretsRecord'sDebug, which prints theAppRoleas"<redacted>");core/src/services.rs(RegistrationSteps).