-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(secrets-vault): Add Secrets Server implementation for Vault
Signed-off-by: Joonas Bergius <joonas@cosmonic.com>
- Loading branch information
Showing
8 changed files
with
1,111 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
name: Secrets Vault | ||
|
||
permissions: | ||
contents: read | ||
|
||
on: | ||
push: | ||
branches: | ||
- "main" | ||
pull_request: | ||
branches: | ||
- "main" | ||
|
||
defaults: | ||
run: | ||
shell: bash | ||
working-directory: ./secrets/secrets-vault | ||
|
||
jobs: | ||
check: | ||
runs-on: ubuntu-latest | ||
steps: | ||
- uses: actions/checkout@v4 | ||
|
||
- name: Lint | ||
run: | | ||
cargo clippy -- --no-deps | ||
- name: Test | ||
run: | | ||
cargo test | ||
- name: Build | ||
run: | | ||
cargo build --release |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
[package] | ||
name = "secrets-vault" | ||
version = "0.1.0" | ||
edition = "2021" | ||
|
||
[dependencies] | ||
anyhow = { version = "1.0.86", default-features = false, features = ["std"] } | ||
# TODO: re-enable upstream async-nats once it supports the feature set needed by wRPC. | ||
# async-nats = { version = "0.35", default-features = false, features = ["ring", "server_2_10"] } | ||
async-nats = { package = "async-nats-wrpc", version = "0.35.1", features = ["ring", "server_2_10"] } | ||
axum = { version = "0.7.5", default-features = false, features = ["http1", "json", "tokio", "tracing"] } | ||
bytes = { version = "1", default-features = false } | ||
clap = { version = "4.5.4", features = ["derive", "env", "string"] } | ||
ed25519-dalek = { version = "2.1.1", features = ["alloc", "pkcs8"] } | ||
futures = { version = "0.3.30", default-features = false, features = [] } | ||
jsonwebtoken = { version = "9.3.0" } | ||
nkeys = { git = "https://github.com/wasmcloud/nkeys.git", features = ["jwk", "xkeys"], rev = "7e52c00793bd1c10a0fb8ad17acdf6e6e1f76ae6" } | ||
nkeys_041 = { package = "nkeys", version = "0.4.1", features = ["xkeys"] } | ||
serde = { version = "1.0.203", default-features = false, features = ["std"] } | ||
serde_json = { version = "1.0.117", default-features = false, features = ["std"] } | ||
tokio = { version = "1.38.0", default-features = false, features = ["full"] } | ||
tracing = { version = "0.1.40", default-features = false, features = [] } | ||
tracing-subscriber = { version = "0.3.18", default-features = false, features = ["fmt", "env-filter"] } | ||
vaultrs = { version = "0.7.2", default-features = false, features = ["rustls"] } | ||
wasmcloud-secrets-client = { version = "*", git = "https://github.com/wasmcloud/wasmCloud.git", branch = "feat/host-secrets-impl" } | ||
wasmcloud-secrets-types = { version = "0.1.0", git = "https://github.com/wasmcloud/wasmCloud.git", branch = "feat/host-secrets-impl" } | ||
wascap = { version = "0.15.0" } | ||
|
||
[dev-dependencies] | ||
testcontainers = { version = "0.20.0", default-features = false } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
# secrets-vault | ||
|
||
[wasmCloud Secrets][wasmcloud-secrets] Server implementation for HashiCorp Vault that uses the [JWT Auth method][vault-jwt-auth] to fetch secrets stored in the [KV Secrets Engine - version 2][vault-kv2-secrets]. | ||
|
||
## How it works | ||
|
||
When the Secrets Vault Server starts, it does the following: | ||
|
||
1. Subscribes to NATS messages on the configured wasmCloud secrets subject (`wasmcloud.secrets.v0.<service-name>` by default) for Server Xkey requests on `<wasmcloud-secrets-subject>.server_xkey` and for Secret Requests from wasmCloud hosts on `<wasmcloud-secrets-subject>.get`. | ||
2. Starts serving a JWKS endpoint (on `http://<jwks-address-flag>/.well-known/keys`) that lists JWKs used to sign the authentication requests sent to Vault for fetching the secrets described in incoming `SecretRequest`s. | ||
|
||
### Life of a `SecretRequest` | ||
|
||
![Life of a SecretRequest](./static/life-of-a-secretrequest.png) | ||
|
||
When the server receives a `SecretRequest` via the NATS subject (`wasmcloud.secrets.v0.<service-name>.get`), it runs through the following order of operations: | ||
|
||
1. wasmCloud Host sends a `SecretRequest` to Secrets Vault Server. | ||
2. Secrets Vault Server attempts to decrypt the `SecretRequest` using it's own XKey and the requesting wasmCloud Host's public key attached to the request and proceeds to validate the attached Host and Entity claims. | ||
* Entity refers to either a Component or Provider depending on which the Host is making the SecretRequest for. | ||
3. Secrets Vault Server calls Vault with the [`jwt` authentication method][vault-jwt-auth] using a JWT token derived from the claims attached to the `SecretRequest`. See [Vault Authentication][] section for more details. | ||
4. Vault validates that the authentication JWT has been signed with keys listed on the JWKS endpoint served by the Secret Vault Server and then matches the attached claims in JWT against a set of pre-configured [bound claims configuration][vault-bound-claims]. | ||
5. Once Vault has successfully validated the authentication JWT and succesfully matched it against a role, Vault responds with a client token for the Secrets Vault Server to use for fetching secrets. | ||
6. Secrets Vault Server will then attempt to access the secret by referencing `name` and optionally the `version` fields stored in the `SecretRequest`. The `name` field is expected to represent a combination of the KeyValue (version 2) engine mount and the actual path where the secret is stored, represented as `<secret-engine-mount>/<path/to/the/secret>`. | ||
* For example, using the `Usage` section from [Vault's documentation for KV Secrets Engine version 2][vault-kv2-usage] if a secret is stored under mount of `secret` and named `my-secret`, the `name` value in the `SecretRequest` should be set to `secret/my-secret`. | ||
7. Once Secrets Vault Server is able to successfully access the secret from Vault, it will serialize the stored secret data along with the Vault's secret version in a `SecretResponse`, encrypt the resulting payload using it's configured XKey and the wasmCloud Host's public key so that only the wasmCloud host that requested the secret can decrypt it and respond back to the requesting wasmCloud Host with the encrypted payload. | ||
|
||
|
||
[vault-bound-claims]: https://developer.hashicorp.com/vault/docs/auth/jwt#bound-claims | ||
[vault-jwt-auth]: https://developer.hashicorp.com/vault/docs/auth/jwt#jwt-authentication | ||
[vault-kv2-secrets]: https://developer.hashicorp.com/vault/docs/secrets/kv/kv-v2 | ||
[vault-kv2-usage]: https://developer.hashicorp.com/vault/docs/secrets/kv/kv-v2#usage | ||
[wasmcloud-secrets]: https://github.com/wasmCloud/wasmCloud/issues/2190 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
use anyhow::Result; | ||
use axum::{extract::State, http::StatusCode, routing::get, Json, Router}; | ||
use nkeys::{JsonWebKey, KeyPair}; | ||
use serde::{Deserialize, Serialize}; | ||
use std::{net::SocketAddrV4, sync::Arc}; | ||
|
||
#[derive(Debug)] | ||
pub(crate) struct VaultSecretsJwksServer { | ||
keys: Vec<JsonWebKey>, | ||
listen_address: SocketAddrV4, | ||
} | ||
|
||
struct SharedState { | ||
keys: Vec<JsonWebKey>, | ||
} | ||
|
||
#[derive(Debug, Serialize, Deserialize)] | ||
struct JwksResponse { | ||
keys: Vec<JsonWebKey>, | ||
} | ||
|
||
impl VaultSecretsJwksServer { | ||
pub fn new(nkeys: Vec<KeyPair>, listen_address: SocketAddrV4) -> Result<Self> { | ||
let mut keys = vec![]; | ||
for kp in nkeys { | ||
keys.push(JsonWebKey::try_from(kp)?); | ||
} | ||
Ok(Self { | ||
keys, | ||
listen_address, | ||
}) | ||
} | ||
|
||
pub async fn serve(&self) -> Result<()> { | ||
let state = Arc::new(SharedState { | ||
keys: self.keys.clone(), | ||
}); | ||
let app = Router::new() | ||
.route("/.well-known/keys", get(handle_well_known_keys)) | ||
.with_state(state); | ||
|
||
let listener = tokio::net::TcpListener::bind(self.listen_address).await?; | ||
axum::serve(listener, app).await?; | ||
|
||
Ok(()) | ||
} | ||
} | ||
|
||
async fn handle_well_known_keys( | ||
State(state): State<Arc<SharedState>>, | ||
) -> (StatusCode, Json<JwksResponse>) { | ||
( | ||
StatusCode::OK, | ||
Json(JwksResponse { | ||
keys: state.keys.clone(), | ||
}), | ||
) | ||
} |
Oops, something went wrong.