# ExCredstash
[](https://hex.pm/packages/ex_credstash)
[](https://github.com/relaypro-open/ex_credstash/actions/workflows/ci.yml)
[](https://hexdocs.pm/ex_credstash)
Elixir implementation of [credstash](https://github.com/fugue/credstash) -
a utility for managing credentials using AWS KMS and DynamoDB.
## Features
- Full compatibility with Python credstash data format
- Both library and CLI interfaces
- AES-256-CTR encryption with HMAC verification
- Automatic version management
- KMS encryption context support
- Multiple digest algorithms (SHA, SHA224, SHA256, SHA384, SHA512, MD5)
## Installation
### As a Library
Add `ex_credstash` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:ex_credstash, "~> 0.1.0"}
]
end
```
Then run:
```bash
mix deps.get
```
### As a CLI Tool
#### Install from Hex.pm
```bash
mix escript.install hex ex_credstash
```
Make sure `~/.mix/escripts` is in your PATH:
```bash
export PATH="$HOME/.mix/escripts:$PATH"
```
#### Build from Source
```bash
git clone https://github.com/relaypro-open/ex_credstash.git
cd ex_credstash
mix deps.get
mix escript.build
```
This creates a `credstash` binary in the project root. You can move it to a directory in your PATH:
```bash
sudo mv credstash /usr/local/bin/
```
## Usage
### Library Usage
```elixir
# Set up the DynamoDB table
ExCredstash.setup(region: "us-east-1")
# Store a secret
{:ok, version} = ExCredstash.put("db_password", "super_secret", region: "us-east-1")
# Store with specific version
{:ok, "0000000000000000005"} = ExCredstash.put("api_key", "key123",
region: "us-east-1",
version: 5
)
# Store with encryption context
ExCredstash.put("secret", "value",
region: "us-east-1",
context: %{"environment" => "production"}
)
# Store multiple secrets at once
{:ok, versions} = ExCredstash.put_all(%{
"key1" => "val1",
"key2" => "val2"
}, region: "us-east-1")
# Retrieve a secret (latest version)
{:ok, "super_secret"} = ExCredstash.get("db_password", region: "us-east-1")
# Retrieve a specific version
{:ok, "old_secret"} = ExCredstash.get("db_password", region: "us-east-1", version: 1)
# Retrieve all secrets (decrypted)
{:ok, %{"db_password" => "secret1", "api_key" => "secret2"}} =
ExCredstash.get_all(region: "us-east-1")
# List all credentials (metadata only, not decrypted)
{:ok, [%{name: "db_password", version: "0000000000000000001", comment: nil}]} =
ExCredstash.list(region: "us-east-1")
# List unique credential names
{:ok, ["api_key", "db_password"]} = ExCredstash.keys(region: "us-east-1")
# Delete all versions of a secret
{:ok, 3} = ExCredstash.delete("old_secret", region: "us-east-1")
```
### CLI Usage
```bash
# Set up the DynamoDB table
credstash setup -r us-east-1
# Store a secret
credstash put db_password "super_secret" -r us-east-1
# Store with auto-versioning (default behavior)
credstash put api_key "key123" -a -r us-east-1
# Store with explicit version
credstash put api_key "key456" -v 2 -r us-east-1
# Store with custom KMS key
credstash put secret "value" -k alias/my-key -r us-east-1
# Store with encryption context
credstash put secret "value" environment=production app=myapp -r us-east-1
# Store with comment
credstash put api_key "key123" -c "API key for external service" -r us-east-1
# Read secret value from stdin
echo "secret" | credstash put my_secret -r us-east-1
# Read secret value from file
credstash put certificate @/path/to/cert.pem -r us-east-1
# Retrieve a secret
credstash get db_password -r us-east-1
# Retrieve specific version
credstash get db_password -v 1 -r us-east-1
# Retrieve without trailing newline
credstash get db_password -n -r us-east-1
# Retrieve with encryption context
credstash get secret environment=production -r us-east-1
# Retrieve all secrets as JSON
credstash getall -f json -r us-east-1
# Retrieve all secrets as dotenv format
credstash getall -f dotenv -r us-east-1
# List all credentials
credstash list -r us-east-1
# List unique credential names
credstash keys -r us-east-1
# Delete a secret (all versions)
credstash delete old_secret -r us-east-1
# Use a custom table
credstash -t my-secrets list -r us-east-1
```
## Configuration
### Application Config
```elixir
# config/config.exs
config :ex_credstash,
region: "us-east-1",
table: "credential-store",
kms_key: "alias/credstash"
```
### Environment Variables
- `AWS_REGION` or `AWS_DEFAULT_REGION` - AWS region
- `CREDSTASH_TABLE` - DynamoDB table name (default: "credential-store")
- `CREDSTASH_KEY_ID` - KMS key ID or alias (default: "alias/credstash")
### AWS Credentials
ExCredstash uses the standard AWS credential chain:
1. Environment variables (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`)
2. Shared credentials file (`~/.aws/credentials`)
3. IAM role (for EC2 instances, ECS tasks, Lambda functions)
## AWS Setup
### KMS Key
Create a KMS key for credstash:
```bash
aws kms create-key --description "Credstash key"
aws kms create-alias --alias-name alias/credstash --target-key-id <key-id>
```
Or use an existing key by setting the `kms_key` configuration.
### IAM Permissions
The following IAM permissions are required:
**For reading secrets:**
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["kms:Decrypt"],
"Resource": "arn:aws:kms:REGION:ACCOUNT:key/KEY-ID"
},
{
"Effect": "Allow",
"Action": [
"dynamodb:GetItem",
"dynamodb:Query",
"dynamodb:Scan"
],
"Resource": "arn:aws:dynamodb:REGION:ACCOUNT:table/credential-store"
}
]
}
```
**For writing secrets:**
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["kms:GenerateDataKey"],
"Resource": "arn:aws:kms:REGION:ACCOUNT:key/KEY-ID"
},
{
"Effect": "Allow",
"Action": [
"dynamodb:PutItem",
"dynamodb:Query"
],
"Resource": "arn:aws:dynamodb:REGION:ACCOUNT:table/credential-store"
}
]
}
```
**For setup (creating table):**
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"dynamodb:CreateTable",
"dynamodb:DescribeTable",
"dynamodb:TagResource"
],
"Resource": "arn:aws:dynamodb:REGION:ACCOUNT:table/credential-store"
}
]
}
```
**For deleting secrets:**
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"dynamodb:Query",
"dynamodb:DeleteItem"
],
"Resource": "arn:aws:dynamodb:REGION:ACCOUNT:table/credential-store"
}
]
}
```
## Running Tests
### Unit Tests
Unit tests run without AWS credentials and mock AWS SDK responses:
```bash
mix test
```
### Integration Tests
Integration tests verify real AWS operations and require:
1. **AWS Credentials**: Set up via environment variables or `~/.aws/credentials`:
```bash
export AWS_ACCESS_KEY_ID="your-access-key"
export AWS_SECRET_ACCESS_KEY="your-secret-key"
export AWS_REGION="us-east-1"
```
2. **KMS Key**: A KMS key with alias `alias/credstash` (or configure a different key)
3. **Run integration tests**:
```bash
mix test --include integration
```
## Development
```bash
# Run tests
mix test
# Run formatter
mix format
# Check formatting
mix format --check-formatted
# Run type checker
mix dialyzer
# Run linter
mix credo
# Build escript
mix escript.build
# Compile with warnings as errors
mix compile --warnings-as-errors
```
## Contributing
1. Fork the repository
2. Create your feature branch (`git checkout -b feature/my-feature`)
3. Run tests and ensure they pass (`mix test`)
4. Run formatter (`mix format`)
5. Commit your changes (`git commit -am 'Add new feature'`)
6. Push to the branch (`git push origin feature/my-feature`)
7. Create a Pull Request
## Acknowledgments
- [credstash](https://github.com/fugue/credstash) - The original Python implementation
- [AWS Erlang SDK](https://github.com/aws-beam/aws-erlang) - AWS client library