From 186dfea8374f34469b2dc9380c9b1e79b5575036 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Tue, 9 Sep 2025 16:42:27 -0600 Subject: [PATCH] Updated README and forced all secret names to be in UPPER_CAMEL_CASE --- README.md | 279 ++++++++++++++++++++++++++----------------- src/bin/gman/main.rs | 12 +- src/config.rs | 1 - 3 files changed, 175 insertions(+), 117 deletions(-) diff --git a/README.md b/README.md index d922b73..4a87187 100644 --- a/README.md +++ b/README.md @@ -1,150 +1,209 @@ -# gman -A universal credential management CLI with a unified interface for all your secret providers. +# gman - Universal Credential Manager -`gman` provides a single, consistent set of commands to manage secrets, whether they are stored in a secure local vault or any other supported provider. Switch between providers on the fly, script interactions with JSON output, and manage your secrets with ease. +`gman` is a command-line tool designed to streamline credential and secret management for your scripts, automations, and +applications. It provides a single, secure interface to store, retrieve, and inject secrets, eliminating the need to +juggle different methods like configuration files or environment variables for each tool. + +## Overview + +The core philosophy of `gman` is to act as a universal wrapper for any command that requires credentials. You can store +your secrets—like API tokens, passwords, or certificates—in an encrypted vault backed by various providers. Then, you +can either fetch them directly or, more powerfully, execute commands through `gman`, which securely injects the +necessary secrets as environment variables or command-line flags. ## Features -- **Secure Local Storage**: Out-of-the-box support for a local vault (`~/.config/gman/vault.yml`) with strong encryption using **Argon2id** for key derivation and **XChaCha20-Poly1305** for authenticated encryption. -- **Unified Interface**: A consistent command set (`add`, `get`, `list`, etc.) for every supported provider. -- **Provider Selection**: Explicitly choose a provider for a command using the `--provider` flag. -- **Flexible Output**: Get secrets in plaintext for scripting, structured `json` for applications, or human-readable text. -- **Password Management**: For local secret storage: securely prompts for the vault password. For automation, a password can be supplied via a `~/.gman_password` file, similar to Ansible Vault. -- **Shell Completions**: Generate completion scripts for Bash, Zsh, Fish, and other shells. -- **Standardized Naming**: Secret names are automatically converted to `snake_case` to ensure consistency. +- **Secure, Encrypted Storage**: All secrets are stored in an encrypted state using strong cryptography. +- **Pluggable Providers**: Supports different backends for secret storage. The default is a local file-based system. +- **Git Synchronization**: The `local` provider can synchronize your encrypted secrets across multiple systems using a + private Git repository. +- **Seamless Command Wrapping**: Run any command through `gman` to automatically provide it with the secrets it needs + (e.g., `gman aws s3 ls`). +- **Customizable Run Profiles**: Define how secrets are passed to commands, either as environment variables (default) or + as specific command-line flags. +- **Secret Name Standardization**: Enforces `snake_case` for all secret names to ensure consistency. +- **Direct Secret Access**: Retrieve plaintext secrets directly when needed (e.g., `gman get my_api_key`). +- **Dry Run Mode**: Preview the command and the secrets that will be injected without actually executing it using the + `--dry-run` flag. ## Installation -Ensure you have Rust and Cargo installed. Then, clone the repository and install the binary: +### Cargo +If you have Cargo installed, then you can install gman from Crates.io: -```sh -git clone https://github.com/Dark-Alex-17/gman.git -cd gman -cargo install --path . +```shell +cargo install gman + +# If you encounter issues installing, try installing with '--locked' +cargo install --locked gman ``` ## Configuration -`gman` is configured through a YAML file located at `~/.config/gman/config.yml`. +`gman` is configured via a YAML file located somewhere different for each OS: -A default configuration is created automatically. Here is an example: +### Linux +``` +$HOME/.config/gman/config.yml +``` + +### Mac +``` +$HOME/Library/Application Support/gman/config.yml +``` + +### Windows +``` +%APPDATA%/Roaming/gman/config.yml +``` + +### Default Configuration ```yaml -# ~/.config/gman/config.yml --- provider: local -password_file: null # Can be set to a path like /home/user/.gman_password +password_file: ~/.gman_password + +# Optional Git sync settings for the 'local' provider +git_branch: null # Defaults to 'main' +git_remote_url: null # Required for Git sync +git_user_name: null # Defaults to global git config user.name +git_user_email: null # Defaults to global git config user.email +git_executable: null # Defaults to 'git' in PATH +run_configs: null # List of run configurations (profiles) ``` -### Vault File +### Provider: `local` -For the `local` provider, secrets are stored in an encrypted vault file at `~/.config/gman/vault.yml`. This file should not be edited manually. +The default `local` provider stores an encrypted vault file on your filesystem. For use across multiple systems, it can +sync with a remote Git repository. -### Password File +**Important Notes for Git Sync:** +- You **must** create the remote repository on your Git provider (e.g., GitHub) *before* attempting to sync. +- The `git_remote_url` must be in SSH or HTTPS format (e.g., `git@github.com:your-user/your-repo.git`). -To avoid being prompted for a password with every command, you can create a file at `~/.gman_password` containing your vault password. `gman` will automatically detect and use this file if it exists. - -```sh -# Create the password file with the correct permissions -echo "your-super-secret-password" > ~/.gman_password -chmod 600 ~/.gman_password +**Example `local` provider config for Git sync:** +```yaml +provider: local +git_branch: main +git_remote_url: "git@github.com:my-user/gman-secrets.git" +git_user_name: "Your Name" +git_user_email: "your.email@example.com" ``` +### Run Configurations + +Run configurations (or "profiles") tell `gman` how to inject secrets into a command. When you run `gman `, it +looks for a profile with a `name` matching ``. If found, it injects the specified secrets. If no profile is +found, `gman` will error out and report that it could not find the run config with that name. + +#### Important: Secret names are always injected in `UPPER_SNAKE_CASE` format. + +#### Basic Run Config (Environment Variables) + +By default, secrets are injected as environment variables. The two required fields are `name` and `secrets`. + +**Example:** A profile for the `aws` CLI. +```yaml +run_configs: + - name: aws + secrets: + - aws_access_key_id + - aws_secret_access_key +``` +When you run `gman aws ...`, `gman` will fetch these two secrets and expose them as environment variables to the `aws` +process. + +#### Advanced Run Config (Command-Line Flags) + +For applications that don't read environment variables, you can configure `gman` to pass secrets as command-line flags. +This requires three additional fields: `flag`, `flag_position`, and `arg_format`. + +- `flag`: The flag to use (e.g., `-e`). +- `flag_position`: An integer indicating where to insert the flag in the command's arguments. `1` is immediately after + the command name. +- `arg_format`: A string that defines how the secret is formatted. It **must** contain the placeholders `{key}` and + `{value}`. + +**Example:** A profile for `docker run` that uses the `-e` flag. +```yaml +run_configs: + - name: docker + secrets: + - my_app_api_key + - my_app_db_password + flag: -e + flag_position: 2 # In 'docker run ...', the flag comes after 'run', so position 2. + arg_format: "{key}={value}" +``` +When you run `gman docker run my-image`, `gman` will execute a command similar to: +`docker run -e MY_APP_API_KEY=... -e MY_APP_DB_PASSWORD=... my-image` + ## Usage -`gman` uses simple commands to manage secrets. Secret values are passed via `stdin`. +### Storing and Managing Secrets -### Commands +All secret names are automatically converted to `snake_case`. -**1. Add a Secret** +- **Add a secret:** + ```sh + # The value is read from standard input + echo "your-secret-value" | gman add my_api_key + ``` + or don't provide a value to add the secret interactively: + ```shell + gman add my_api_key + ``` -To add a new secret, use the `add` command. You will be prompted to enter the secret value, followed by `Ctrl-D` to save. +- **Retrieve a secret:** + ```sh + gman get my_api_key + ``` -```sh -gman add my_api_key -``` -``` -Enter the text to encrypt, then press Ctrl-D twice to finish input -this-is-my-secret-api-key -^D -✓ Secret 'my_api_key' added to the vault. -``` +- **Update a secret:** + ```sh + echo "new-secret-value" | gman update my_api_key + ``` + or don't provide a value to update the secret interactively: + ```shell + gman add my_api_key + ``` -You can also pipe the value directly: -```sh -echo "this-is-my-secret-api-key" | gman add my_api_key -``` +- **List all secret names:** + ```sh + gman list + ``` -**2. Get a Secret** +- **Delete a secret:** + ```sh + gman delete my_api_key + ``` -Retrieve a secret's plaintext value with the `get` command. +- **Synchronize with remote secret storage (specific to the configured `provider`):** + ```sh + gman sync + ``` -```sh -gman get my_api_key -``` -``` -this-is-my-secret-api-key -``` +### Running Commands -**3. Get a Secret as JSON** +- **Using a default profile:** + ```sh + # If an 'aws' profile exists, secrets are injected. + gman aws sts get-caller-identity + ``` -Use the `--output json` flag to get the secret in a structured format. +- **Specifying a profile:** + ```sh + # Manually specify which profile to use with --profile + gman --profile my-docker-profile docker run my-app + ``` -```sh -gman get my_api_key --output json -``` -``` -{ - "my_api_key": "this-is-my-secret-api-key" -} -``` - -**4. List Secrets** - -List the names of all secrets in the vault. - -```sh -gman list -``` -``` -Secrets in the vault: -- my_api_key -- another_secret -``` - -**5. Update a Secret** - -Update an existing secret's value. - -```sh -echo "new-secret-value" | gman update my_api_key -``` -``` -✓ Secret 'my_api_key' updated in the vault. -``` - -**6. Delete a Secret** - -Remove a secret from the vault. - -```sh -gman delete my_api_key -``` -``` -✓ Secret 'my_api_key' deleted from the vault. -``` - -**7. Generate Shell Completions** - -Create a completion script for your shell to enable auto-complete for commands and arguments. - -```sh -# For Bash -gman completions bash > /etc/bash_completion.d/gman - -# For Zsh -gman completions zsh > /usr/local/share/zsh/site-functions/_gman -``` +- **Dry Run:** + ```sh + # See what command would be executed without running it. + gman --dry-run aws s3 ls + # Output will show: aws -e AWS_ACCESS_KEY_ID=***** ... s3 ls + ``` ## Creator * [Alex Clarke](https://github.com/Dark-Alex-17) \ No newline at end of file diff --git a/src/bin/gman/main.rs b/src/bin/gman/main.rs index 2cc310f..929f037 100644 --- a/src/bin/gman/main.rs +++ b/src/bin/gman/main.rs @@ -140,7 +140,7 @@ fn main() -> Result<()> { Commands::Add { name } => { let plaintext = read_all_stdin().with_context(|| "unable to read plaintext from stdin")?; - let snake_case_name = name.to_snake_case(); + let snake_case_name = name.to_snake_case().to_uppercase(); secrets_provider .set_secret(&config, &snake_case_name, plaintext.trim_end()) .map(|_| match cli.output { @@ -149,7 +149,7 @@ fn main() -> Result<()> { })?; } Commands::Get { name } => { - let snake_case_name = name.to_snake_case(); + let snake_case_name = name.to_snake_case().to_uppercase(); secrets_provider .get_secret(&config, &snake_case_name) .map(|secret| match cli.output { @@ -171,7 +171,7 @@ fn main() -> Result<()> { Commands::Update { name } => { let plaintext = read_all_stdin().with_context(|| "unable to read plaintext from stdin")?; - let snake_case_name = name.to_snake_case(); + let snake_case_name = name.to_snake_case().to_uppercase(); secrets_provider .update_secret(&config, &snake_case_name, plaintext.trim_end()) .map(|_| match cli.output { @@ -180,7 +180,7 @@ fn main() -> Result<()> { })?; } Commands::Delete { name } => { - let snake_case_name = name.to_snake_case(); + let snake_case_name = name.to_snake_case().to_uppercase(); secrets_provider .delete_secret(&snake_case_name) .map(|_| match cli.output { @@ -280,13 +280,13 @@ pub fn wrap_and_run_command( .expect("no secrets configured for run profile") .iter() .map(|key| { - let secret_name = key.to_snake_case(); + let secret_name = key.to_snake_case().to_uppercase(); debug!( "Retrieving secret '{secret_name}' for run profile '{}'", run_config_profile_name ); secrets_provider - .get_secret(&config, key.to_snake_case().as_str()) + .get_secret(&config, key.to_snake_case().to_uppercase().as_str()) .ok() .map_or_else( || { diff --git a/src/config.rs b/src/config.rs index 1956a1c..5d2f29d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -57,7 +57,6 @@ pub struct Config { pub provider: SupportedProvider, pub password_file: Option, pub git_branch: Option, - /// The git remote URL to push changes to (e.g. git@github.com:user/repo.git) pub git_remote_url: Option, pub git_user_name: Option, #[validate(email)]