Skip to main content

README.md

# ExCredstash

[![Hex.pm](https://img.shields.io/hexpm/v/ex_credstash.svg)](https://hex.pm/packages/ex_credstash)
[![CI](https://github.com/relaypro-open/ex_credstash/actions/workflows/ci.yml/badge.svg)](https://github.com/relaypro-open/ex_credstash/actions/workflows/ci.yml)
[![Documentation](https://img.shields.io/badge/docs-hexpm-blue.svg)](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