Skip to content
This repository has been archived by the owner on Dec 19, 2024. It is now read-only.

Commit

Permalink
Merge pull request #1 from smolse/initial-implementation
Browse files Browse the repository at this point in the history
Initial implementation
  • Loading branch information
Sergei Smolianinov authored Aug 7, 2023
2 parents 08d99f9 + 2d149e7 commit 0421914
Show file tree
Hide file tree
Showing 11 changed files with 570 additions and 2 deletions.
37 changes: 37 additions & 0 deletions .github/workflows/pr-created.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: PR Created

on:
pull_request:
branches: [ main ]

jobs:
build:
name: Build
runs-on: ubuntu-22.04

steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0

- name: Install Rust stable toolchain
run: rustup toolchain install stable --profile minimal

- name: Cache Rust workspaces
uses: Swatinem/rust-cache@v2
with:
workspaces: |
. -> target
- name: Check formatting
run: cargo fmt --check

- name: Run linter
run: cargo clippy --all-targets --all-features -- -W clippy::cognitive_complexity -D warnings

- name: Run unit tests
run: cargo test

- name: Build
run: cargo build --release
22 changes: 22 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[package]
name = "vector-aws-secrets-helper"
version = "0.1.0"
edition = "2021"

[profile.release]
opt-level = "z" # Optimize for size
lto = true # Enable link-time optimization
codegen-units = 1 # Reduce number of codegen units to increase optimizations
panic = "abort" # Abort on panic
strip = true # Strip symbols from binary

[dependencies]
async-trait = "0.1.72"
aws-config = "0.56.0"
aws-sdk-secretsmanager = "0.29.0"
aws-sdk-ssm = "0.29.0"
clap = { version = "4.3.19", features = ["derive"] }
futures = "0.3.28"
serde = { version = "1.0.182", features = ["derive"] }
serde_json = "1.0.104"
tokio = { version = "1.29.1", features = ["macros", "rt", "rt-multi-thread"] }
43 changes: 41 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,41 @@
# vector-cloud-secrets-helper
A helper tool for Vector to retrieve secrets from AWS, GCP and Azure using the exec backend
# vector-aws-secrets-helper

A helper tool for [Vector](https://vector.dev/) to securely retrieve secrets from AWS SSM Parameter Store and AWS
Secrets Manager using the [exec](https://vector.dev/docs/reference/configuration/global-options/#secret.exec) backend.

## Installation

Download an executable for the target platform from the
[releases page](https://github.com/smolse/vector-aws-secrets-helper/releases) or clone the repo and build it with the
`cargo build` command. Place the executable in a directory that is in your (or, actually, in the Vector user's) `PATH`
environment variable, e.g. `/usr/local/bin`.

## Usage

Once the executable is installed, it can be used as described in the
[Vector documentation](https://vector.dev/docs/reference/configuration/global-options/#secret.exec). The tool uses
the [default credential provider chain](https://docs.aws.amazon.com/sdkref/latest/guide/standardized-credentials.html#credentialProviderChain)
to authenticate to AWS.

Here is an example configuration for the `exec` secrets backend in Vector:

```toml
[secret.aws_ssm]
type = "exec"
command = ["/usr/local/bin/vector-aws-secrets-helper", "ssm"]

[secret.aws_secrets_manager]
type = "exec"
command = ["/usr/local/bin/vector-aws-secrets-helper", "secretsmanager"]
```

## Limitations

While it's idiomatic to use `/` in the names of SSM Parameter Store parameters and Secrets Manager secrets to create a
hierarchy, Vector currently does not support slashes in the secret names. The only supported characters are
alphanumeric, underscores and dots. Here are some examples of valid secret references (for both SSM Parameter Store and
Secrets Manager):
- `SECRET[aws_ssm.secret]`
- `SECRET[aws_ssm.another_one]`
- `SECRET[aws_ssm.one.more]`
- `SECRET[aws_ssm..secret.with.a.leading.comma]`
1 change: 1 addition & 0 deletions clippy.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
cognitive-complexity-threshold = 10
3 changes: 3 additions & 0 deletions rustfmt.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
max_width = 100
tab_spaces = 4
edition = "2021"
10 changes: 10 additions & 0 deletions src/aws/loader.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
//! This module contains a trait that should be implemented by all secret loader implementations.
use crate::vector::{FetchedSecrets, SecretsToFetch};
use async_trait::async_trait;

/// A trait for loading secrets from AWS backends.
#[async_trait]
pub trait LoadSecrets {
async fn load(&self, secrets: SecretsToFetch) -> FetchedSecrets;
}
3 changes: 3 additions & 0 deletions src/aws/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub mod loader;
pub mod secretsmanager;
pub mod ssm;
144 changes: 144 additions & 0 deletions src/aws/secretsmanager.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
//! This module contains the secrets loader implementation for AWS Secrets Manager.
use crate::vector::{FetchedSecret, FetchedSecrets, SecretsToFetch};
use crate::LoadSecrets;
use async_trait::async_trait;
use aws_sdk_secretsmanager::error::SdkError::ServiceError;
use aws_sdk_secretsmanager::Client;

/// A trait for fetching a single secret from AWS Secrets Manager.
#[async_trait]
pub trait SecretsManagerFetchSecret {
async fn fetch_secret(&self, name: String) -> FetchedSecret;
}

/// Implement the SecretsManagerGetSecret trait for the AWS SDK Secrets Manager client.
#[async_trait]
impl SecretsManagerFetchSecret for Client {
async fn fetch_secret(&self, name: String) -> FetchedSecret {
match self.get_secret_value().secret_id(name).send().await {
Ok(response) => match response.secret_string {
Some(secret) => FetchedSecret {
value: Some(secret),
error: None,
},
None => FetchedSecret {
value: None,
error: Some(String::from("secret not found")),
},
},
Err(error) => match error {
ServiceError(error) => FetchedSecret {
value: None,
error: Some(format!("service error: {}", error.into_err())),
},
_ => FetchedSecret {
value: None,
error: Some(error.to_string()),
},
},
}
}
}

/// A struct for loading secrets from AWS Secrets Manager.
pub struct SecretsManagerSecretsLoader {
client: Box<dyn SecretsManagerFetchSecret + Send + Sync>,
}

/// Implement the SecretsManagerSecretsLoader constructor.
impl SecretsManagerSecretsLoader {
pub fn new(client: impl SecretsManagerFetchSecret + Send + Sync + 'static) -> Self {
Self {
client: Box::new(client),
}
}
}

/// Implement the LoadSecrets trait for SecretsManagerSecretsLoader.
#[async_trait]
impl LoadSecrets for SecretsManagerSecretsLoader {
async fn load(&self, secrets: SecretsToFetch) -> FetchedSecrets {
let create_task = |secret_name: String| {
let secret_to_fetch = secret_name.clone();
let task = async { self.client.fetch_secret(secret_to_fetch).await };
(secret_name, task)
};

// Run tasks concurrently.
let (secret_names, tasks): (Vec<_>, Vec<_>) =
secrets.secrets.into_iter().map(create_task).unzip();
let results: Vec<_> = futures::future::join_all(tasks).await;

// Create a FetchedSecrets struct from the results.
let mut fetched_secrets = FetchedSecrets::default();
secret_names
.into_iter()
.zip(results)
.for_each(|(secret_name, result)| {
fetched_secrets.0.insert(secret_name, result);
});

fetched_secrets
}
}

#[cfg(test)]
mod tests {
use super::*;

#[tokio::test]
async fn ssm_secrets_loader_loads_secrets() {
struct MockSecretsManagerFetchSecret {}

#[async_trait]
impl SecretsManagerFetchSecret for MockSecretsManagerFetchSecret {
async fn fetch_secret(&self, name: String) -> FetchedSecret {
match name.as_str() {
"test.secret_1" => FetchedSecret {
value: Some("qwerty".to_string()),
error: None,
},
"test.secret_2" => FetchedSecret {
value: None,
error: Some("failed to fetch".to_string()),
},
_ => unreachable!(),
}
}
}

let secrets_to_fetch = SecretsToFetch {
version: String::from("1.0"),
secrets: vec![String::from("test.secret_1"), String::from("test.secret_2")],
};

let secrets_loader = SecretsManagerSecretsLoader::new(MockSecretsManagerFetchSecret {});
let fetched_secrets = secrets_loader.load(secrets_to_fetch).await;

assert_eq!(
fetched_secrets,
FetchedSecrets(
[
(
"test.secret_1".to_string(),
FetchedSecret {
value: Some("qwerty".to_string()),
error: None,
}
),
(
"test.secret_2".to_string(),
FetchedSecret {
value: None,
error: Some("failed to fetch".to_string()),
}
)
]
.iter()
.cloned()
.collect()
)
);
}
}
Loading

0 comments on commit 0421914

Please sign in to comment.