diff --git a/otdfctl/cmd/policy/kasKeys.go b/otdfctl/cmd/policy/kasKeys.go index 5c1c408b62..061907970d 100644 --- a/otdfctl/cmd/policy/kasKeys.go +++ b/otdfctl/cmd/policy/kasKeys.go @@ -877,6 +877,7 @@ func initKASKeysCommands() { createDoc.GetDocFlag("private-key-pem").Description, ) injectLabelFlags(&createDoc.Command, false) + createDoc.MarkSensitiveFlags() // Get Kas Key getDoc := man.Docs.GetCommand("policy/kas-registry/key/get", @@ -995,6 +996,7 @@ func initKASKeysCommands() { rotateDoc.GetDocFlag("private-key-pem").Description, ) injectLabelFlags(&rotateDoc.Command, true) + rotateDoc.MarkSensitiveFlags() // Import Kas Key importDoc := man.Docs.GetCommand("policy/kas-registry/key/import", @@ -1049,6 +1051,7 @@ func initKASKeysCommands() { importDoc.GetDocFlag("legacy").Description, ) injectLabelFlags(&importDoc.Command, false) + importDoc.MarkSensitiveFlags() mappingsDoc := man.Docs.GetCommand("policy/kas-registry/key/list-mappings", man.WithRun(policyListKeyMappings), diff --git a/otdfctl/docs/man/policy/kas-registry/key/create.md b/otdfctl/docs/man/policy/kas-registry/key/create.md index 7727d187ed..7c5aea0d99 100644 --- a/otdfctl/docs/man/policy/kas-registry/key/create.md +++ b/otdfctl/docs/man/policy/kas-registry/key/create.md @@ -23,8 +23,10 @@ command: description: Identifier related to the wrapping key. Its meaning depends on the `mode`. For `local` mode, it's a descriptive ID for the `wrappingKey` you provide. For `provider` or `remote` mode, it's the ID of the key within the external provider/system used for wrapping. - name: wrapping-key shorthand: w + sensitive: true description: The symmetric key material (AES cipher, hex encoded) used to wrap the generated private key. Primarily used when `mode` is `local`. - name: private-key-pem + sensitive: true description: The private key PEM (encrypted by an AES 32-byte key, then base64 encoded). Used when importing an existing key pair, typically with `provider` mode. - name: provider-config-id shorthand: p diff --git a/otdfctl/docs/man/policy/kas-registry/key/import.md b/otdfctl/docs/man/policy/kas-registry/key/import.md index a75e4dd48b..e7cc0a7595 100644 --- a/otdfctl/docs/man/policy/kas-registry/key/import.md +++ b/otdfctl/docs/man/policy/kas-registry/key/import.md @@ -20,9 +20,11 @@ command: required: true - name: wrapping-key shorthand: w + sensitive: true description: The symmetric key material (AES cipher, hex encoded) used to wrap the imported private key. required: true - name: private-key-pem + sensitive: true description: The base64 encoded private key PEM to import required: true - name: public-key-pem diff --git a/otdfctl/docs/man/policy/kas-registry/key/rotate.md b/otdfctl/docs/man/policy/kas-registry/key/rotate.md index 860827e5eb..09ec687906 100644 --- a/otdfctl/docs/man/policy/kas-registry/key/rotate.md +++ b/otdfctl/docs/man/policy/kas-registry/key/rotate.md @@ -30,8 +30,10 @@ command: description: Identifier related to the wrapping key. Its meaning depends on the `mode`. For `local` mode, it's a descriptive ID for the `wrappingKey` you provide. For `provider` or `remote` mode, it's the ID of the key within the external provider/system used for wrapping. - name: wrapping-key shorthand: w - description: The symmetric key material (AES cipher, base64 encoded) used to wrap the generated private key. Primarily used when `mode` is `local`. + sensitive: true + description: The symmetric key material (AES cipher, hex encoded) used to wrap the generated private key. Primarily used when `mode` is `local`. - name: private-key-pem + sensitive: true description: The private key PEM (encrypted by an AES 32-byte key, then base64 encoded). Used when importing an existing key pair, typically with `provider` mode. - name: provider-config-id shorthand: p @@ -54,7 +56,7 @@ This command replaces an existing key with a new one while maintaining reference Rotate an existing key to a new key in local mode, where the KAS generates the key pair and the private key is wrapped by the provided `wrappingKey`: ```shell -otdfctl policy kas-registry key rotate --key "old-key-id" --kas "https://kas.example.com/kas" --key-id "new-key-v2" --algorithm "rsa:2048" --mode "local" --wrapping-key-id "virtru-stored-key" --wrapping-key "YWVzIGtleQ==" +otdfctl policy kas-registry key rotate --key "old-key-id" --kas "https://kas.example.com/kas" --key-id "new-key-v2" --algorithm "rsa:2048" --mode "local" --wrapping-key-id "virtru-stored-key" --wrapping-key "a8c4824daafcfa38ed0d13002e92b08720e6c4fcee67d52e954c1a6e045907d1" ``` ### Rotate a key in `provider` mode diff --git a/otdfctl/pkg/man/docflags.go b/otdfctl/pkg/man/docflags.go index 4af5388403..1b1b5b6b09 100644 --- a/otdfctl/pkg/man/docflags.go +++ b/otdfctl/pkg/man/docflags.go @@ -6,12 +6,18 @@ import ( "github.com/opentdf/platform/otdfctl/pkg/cli" ) +// SensitiveAnnotationKey is the pflag annotation key used to mark flags whose +// values contain secrets (cryptographic keys, tokens, etc.) and must not appear +// in logs or process listings. +const SensitiveAnnotationKey = "sensitive" + type DocFlag struct { Name string `yaml:"name"` Description string `yaml:"description"` Shorthand string `yaml:"shorthand"` Default string `yaml:"default"` Enum []string `yaml:"enum"` + Sensitive bool `yaml:"sensitive"` } func (d *Doc) GetDocFlag(name string) DocFlag { @@ -29,3 +35,16 @@ func (d *Doc) GetDocFlag(name string) DocFlag { func (f DocFlag) DefaultAsBool() bool { return f.Default == "true" } + +// MarkSensitiveFlags sets pflag annotations on all flags in the command's +// FlagSet that are marked sensitive in the doc metadata. Call after all +// flags have been registered. +func (d *Doc) MarkSensitiveFlags() { + for _, df := range d.DocFlags { + if df.Sensitive { + if err := d.Flags().SetAnnotation(df.Name, SensitiveAnnotationKey, []string{"true"}); err != nil { + panic(fmt.Sprintf("failed to mark flag %q as sensitive for command %q: %v", df.Name, d.Use, err)) + } + } + } +} diff --git a/otdfctl/pkg/man/docflags_test.go b/otdfctl/pkg/man/docflags_test.go new file mode 100644 index 0000000000..0605817961 --- /dev/null +++ b/otdfctl/pkg/man/docflags_test.go @@ -0,0 +1,78 @@ +package man + +import ( + "testing" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDocFlagSensitiveParsing(t *testing.T) { + doc, err := ProcessDoc(`--- +title: Test Command +command: + name: test + flags: + - name: wrapping-key + sensitive: true + description: A sensitive flag + - name: algorithm + description: A non-sensitive flag +--- + +Test doc body. +`) + require.NoError(t, err) + require.Len(t, doc.DocFlags, 2) + + wk := doc.GetDocFlag("wrapping-key") + assert.True(t, wk.Sensitive) + + alg := doc.GetDocFlag("algorithm") + assert.False(t, alg.Sensitive) +} + +func TestMarkSensitiveFlags(t *testing.T) { + doc, err := ProcessDoc(`--- +title: Test Command +command: + name: test + flags: + - name: wrapping-key + sensitive: true + description: Sensitive + - name: name + description: Not sensitive +--- + +Body. +`) + require.NoError(t, err) + + doc.Flags().String("wrapping-key", "", "Sensitive") + doc.Flags().String("name", "", "Not sensitive") + + doc.MarkSensitiveFlags() + + wkFlag := doc.Flags().Lookup("wrapping-key") + require.NotNil(t, wkFlag) + assert.Equal(t, []string{"true"}, wkFlag.Annotations[SensitiveAnnotationKey]) + + nameFlag := doc.Flags().Lookup("name") + require.NotNil(t, nameFlag) + assert.Nil(t, nameFlag.Annotations) +} + +func TestMarkSensitiveFlagsPanicsOnUnregistered(t *testing.T) { + doc := &Doc{ + Command: cobra.Command{Use: "test"}, + DocFlags: []DocFlag{ + {Name: "missing-flag", Sensitive: true}, + }, + } + + assert.Panics(t, func() { + doc.MarkSensitiveFlags() + }) +}