Skip to content

secrets: add doctl commands for Secrets Manager#1864

Open
MaxLivesInTheMatrix wants to merge 5 commits into
digitalocean:mainfrom
MaxLivesInTheMatrix:secprod-511-secrets-support
Open

secrets: add doctl commands for Secrets Manager#1864
MaxLivesInTheMatrix wants to merge 5 commits into
digitalocean:mainfrom
MaxLivesInTheMatrix:secprod-511-secrets-support

Conversation

@MaxLivesInTheMatrix

@MaxLivesInTheMatrix MaxLivesInTheMatrix commented Jun 18, 2026

Copy link
Copy Markdown

Add create, list, get, update, delete, restore, and list-versions commands with interactive prompts for region, name, and version where appropriate. I find that a lot of CLI tools can be clunky to use. This was written with the goal in mind of DO Simple, where the CLI will even guide you to fix your mistakes instead of erroring out immediately.
All testing was done locally.
Starting with doctl secrets create:
The user can either go through the tags one by one which is automated by the CLI or they can pass in all arguments at once (like if they had a script doing it instead of manually)
One liner:
doctl secrets create test --region nyc3 --value hello=world --value helloagain=world
Manual workflow:
image

Users can list all of their secret containers by:
doctl secrets list

Users can retrieve their encrypted secrets from their containers by:
doctl secrets get hello --region nyc3
or the workflow:
image

Users can delete and restore secrets using simple:
doctl secrets delete <name> --region <region>
doctl secrets restore <name> --region <region>
Or the exact same workflows that are shown above will prompt the user to take the correct actions

Updating a secret requires the secret name, region, and version.
Once again users can one shot this or follow the workflow:
image
It's important to keep in mind that updating a secret container replaces all of the information inside that container with what is being added in the command. this is shown to the user within the workflow.

Also the most basic command: doctl secrets --help
image

MaxLivesInTheMatrix and others added 4 commits June 18, 2026 11:09
Add create, list, get, update, delete, restore, and list-versions commands
with interactive prompts for region, name, and version where appropriate.
Resolve conflicts with main by keeping secrets support alongside the new
VectorDBs commands and godo v1.197.0.

Co-authored-by: Cursor <cursoragent@cursor.com>
@MaxLivesInTheMatrix MaxLivesInTheMatrix marked this pull request as ready for review June 25, 2026 18:14
Comment thread commands/secrets.go
return c.Display(&displayers.SecretWriteResult{Result: *result})
}

// RunCmdSecretsDelete schedules a secret container for deletion.

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.

delete has no confirmation or --force

Every other destructive command in doctl gates deletion behind a confirmation prompt and a --force flag (see RunVolumeDelete in volumes.go). This deletes immediately with no guardrail. Even though it's a soft delete with restore, we should stay consistent with the rest of the CLI and protect against fat-fingering the wrong secret/region.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done. secrets delete now uses AskForConfirmDelete like the other destructive commands, with --force / -f to skip the prompt.

Comment thread commands/secrets.go
return c.Display(&displayers.SecretVersions{Versions: versions})
}

// RunCmdSecretsUpdate updates a secret container.

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.

full-replace can silently drop keys

update replaces the entire container, so any key the user forgets to re-supply is silently deleted.
Would it be possible to pick one of 2 -

Preferred: split intent — add secrets set key=val (merge: fetch → apply → write) and secrets unset key, and reserve full-replace for an explicit update --replace.
Minimum: before writing, fetch current keys and require confirmation (gated by --force) when keys would be removed, e.g. This will remove keys: api_key, token. Continue?.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

This was a concern I initially had too, but we intentionally made this very CRUD heavy. This also simplifies how we can delete keys from the secret container. As deleting a key is just sending the entire payload, but with whatever key missing to delete it. I tried to make it explicit as possible to notify users that all of the values get replaced when the users update the secret. It’s a common (if blunt) design for secret stores:

  • Simpler storage — one encrypted blob per version, not per-key CRUD
  • Atomic updates — the new version is a single snapshot
  • Easier versioning — each version is a complete state of the secret
  • Optimistic locking — version guards whole-container writes
  • Tradeoff: callers must send the full desired state, not a delta.

AWS Secrets Manager is quite similar in this regard too. I will bring this up in my team's convos to see what they think.

Comment thread commands/secrets.go Outdated
If no `+"`"+`--value`+"`"+` flags are provided, key-value pairs are read interactively with --interactive. You are prompted for each key, then each value is masked.`, Writer,
aliasOpt("c"), displayerType(&displayers.SecretWriteResult{}))
AddStringFlag(cmdCreate, doctl.ArgRegionSlug, "", "", secretRegionFlagDesc)
AddStringSliceFlag(cmdCreate, doctl.ArgSecretValue, "", nil,

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.

plaintext --value leaks secret material

The only non-interactive way to supply values is --value key=plaintext, which lands secrets in shell history, the process table (ps aux), and CI logs

can we read it from a source as env var , matching vault/kubectl: -


--value password=@./pw.txt   # from file
--value password=-           # from stdin
--from-env-file .env         # bulk

File reading already exists in the codebase (certificates.go).

@MaxLivesInTheMatrix MaxLivesInTheMatrix Jun 26, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done. --value now supports key=@ path and key=- (stdin). Added --from-env-file on create/update; --value flags override env-file keys. Reused the existing file-read pattern from certificates.go.

Comment thread commands/secrets.go Outdated
}

if len(list.UnavailableRegions) > 0 {
notice("Some regions were unavailable: %s", strings.Join(list.UnavailableRegions, ", "))

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.

notice() writes to color.Output (stdout), so secrets list ("Some regions were unavailable…") and secrets update ("Using current version…") emit a Notice: line onto stdout ahead of the JSON/table. That breaks doctl secrets list -o json for scripting. These informational messages should go to os.Stderr (consistent with how the interactive prompts already write).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done. Informational messages for unavailable regions now go to stderr via secretNotice() instead of notice().

Comment thread commands/secrets.go Outdated

secret, err := c.Secrets().Get(name, region)
if err == nil && secret.Version > 0 {
notice("Using current version %d of %q in %s", secret.Version, name, region)

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.

notice() writes to color.Output (stdout), so secrets list ("Some regions were unavailable…") and secrets update ("Using current version…") emit a Notice: line onto stdout ahead of the JSON/table. That breaks doctl secrets list -o json for scripting. These informational messages should go to os.Stderr (consistent with how the interactive prompts already write).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done. The “Using current version…” message also goes to stderr via secretNotice().

Comment thread commands/secrets.go Outdated
)

// secretRegionReader reads interactive region input. It can be replaced in tests.
var secretRegionReader = bufio.NewReader(os.Stdin)

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.

This file mixes two input styles: raw bufio readers on stdin for name/region/version, and charm/input for key/value (plus three separate stdin readers). That's fragile — a bufio reader can read past its own line and swallow input meant for the next prompt, and mixing it with charm (which drives the terminal itself) makes dropped/misplaced input easy. Let's standardize on charm everywhere, like the rest of doctl — input for name/version, list for region. It also lets the global test-only input vars go away.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done. Removed the bufio stdin readers. Name and version use charm/input; region uses charm/list. Dropped the test-only reader vars

Comment thread commands/secrets.go
return c.Display(&displayers.SecretWriteResult{Result: *result})
}

// RunCmdSecretsGet retrieves a secret container.

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.

get prints all decrypted values to stdout by default, and the only way to read one value is to print them all and grep. Consider masking by default with --show to reveal, and add secrets get --key password --raw for safe piping (no table chrome / trailing-newline surprises).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done. Values are masked by default (********). Added --show to reveal, and --key + --raw for safe scripting (raw prints only the value, no table formatting).

Adds in confirm to deletes
--value now supports key=@path and key=- (stdin). Added --from-env-file on create/update; --value flags override env-file keys.
Removed the bufio stdin readers. Name and version use charm/input; region uses charm/list. Dropped the test-only reader vars.
Values are masked by default (********). Added --show to reveal, and --key + --raw for safe scripting (raw prints only the value, no table formatting).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants