-
Notifications
You must be signed in to change notification settings - Fork 6
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
Co-authored-by: Brooks Townsend <brooksmtownsend@gmail.com> Signed-off-by: Joonas Bergius <joonas@cosmonic.com>
- Loading branch information
1 parent
fda93b6
commit e8034b3
Showing
12 changed files
with
1,479 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,99 @@ | ||
name: Secrets Vault | ||
|
||
permissions: | ||
contents: read | ||
|
||
on: | ||
push: | ||
branches: | ||
- "main" | ||
pull_request: | ||
branches: | ||
- "main" | ||
|
||
env: | ||
REGISTRY: ghcr.io | ||
IMAGE_NAME: wasmcloud/contrib/secrets-vault | ||
|
||
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 | ||
release: | ||
if: startswith(github.ref, 'refs/tags/secrets-vault-v') # Only run on tag push | ||
runs-on: ubuntu-latest | ||
needs: | ||
- check | ||
permissions: | ||
contents: read | ||
packages: write | ||
steps: | ||
- uses: actions/checkout@v4 | ||
|
||
- name: Set up QEMU | ||
uses: docker/setup-qemu-action@v3 | ||
|
||
- name: Set up Docker Buildx | ||
uses: docker/setup-buildx-action@v3 | ||
|
||
- name: Log into GitHub Container Registry | ||
uses: docker/login-action@v3 | ||
with: | ||
registry: ${{ env.REGISTRY }} | ||
username: ${{ github.repository_owner }} | ||
password: ${{ secrets.GITHUB_TOKEN }} | ||
|
||
- name: Extract metadata (tags, labels) | ||
id: meta_release | ||
uses: docker/metadata-action@v5 | ||
with: | ||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} | ||
tags: | | ||
type=match,pattern=secrets-vault-v(.*),group=1 | ||
- name: Extract metadata (tags, labels) | ||
id: meta_debug | ||
uses: docker/metadata-action@v5 | ||
with: | ||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} | ||
tags: | | ||
type=match,pattern=secrets-vault-v(.*),group=1,suffix=-debug | ||
- name: Build and push the release image | ||
uses: docker/build-push-action@v6 | ||
with: | ||
target: release | ||
push: true | ||
context: secrets/secrets-vault/ | ||
tags: ${{ steps.meta_release.outputs.tags }} | ||
labels: ${{ steps.meta_release.outputs.labels }} | ||
platforms: linux/amd64,linux/arm64 | ||
|
||
- name: Build and push the debug image | ||
uses: docker/build-push-action@v6 | ||
with: | ||
target: debug | ||
push: true | ||
context: secrets/secrets-vault/ | ||
tags: ${{ steps.meta_debug.outputs.tags }} | ||
labels: ${{ steps.meta_debug.outputs.labels }} | ||
platforms: linux/amd64,linux/arm64 |
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 |
---|---|---|
|
@@ -12,3 +12,5 @@ Cargo.lock | |
|
||
# MSVC Windows builds of rustc generate these, which store debugging information | ||
*.pdb | ||
|
||
.DS_Store |
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,4 @@ | ||
Dockerfile* | ||
**/target | ||
target | ||
.cargo |
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 @@ | ||
[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"] } | ||
data-encoding = { version = "2.6.0" } | ||
ed25519-dalek = { version = "2.1.1", features = ["alloc", "pkcs8"] } | ||
futures = { version = "0.3.30", default-features = false, features = [] } | ||
jsonwebtoken = { version = "9.3.0" } | ||
nkeys = { version = "0.4.2", features = ["xkeys"] } | ||
serde = { version = "1.0.203", default-features = false, features = ["std"] } | ||
serde_json = { version = "1.0.117", default-features = false, features = ["std"] } | ||
sha2 = { version = "0.10.8" } | ||
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"] } | ||
# TODO: update this to use the published crate once it exists. | ||
wasmcloud-secrets-types = { version = "0.2.0", git = "https://github.com/wasmcloud/wasmCloud.git", branch = "fix/secrets-impl-cleanup" } | ||
wascap = { version = "0.15.0" } | ||
|
||
[dev-dependencies] | ||
testcontainers = { version = "0.20.0", default-features = false } | ||
# TODO: update this to use the published crate once it exists. | ||
wasmcloud-secrets-client = { version = "0.1.0", git = "https://github.com/wasmcloud/wasmCloud.git", branch = "fix/secrets-impl-cleanup" } |
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,21 @@ | ||
# syntax=docker/dockerfile:1.4 | ||
FROM rust:1.79-slim-bookworm AS builder | ||
|
||
WORKDIR /build | ||
COPY . /build | ||
|
||
RUN <<EOF | ||
cargo build --release | ||
EOF | ||
|
||
# Debug release using the distroless debug image: | ||
# * https://github.com/GoogleContainerTools/distroless/tree/main?tab=readme-ov-file#debug-images | ||
# | ||
# Includes /busybox/shell for debugging, which should be used to override the default entrypoint. | ||
FROM gcr.io/distroless/cc-debian12:debug AS debug | ||
COPY --from=builder /build/target/release/secrets-vault /bin/secrets-vault | ||
ENTRYPOINT ["/bin/secrets-vault"] | ||
|
||
FROM gcr.io/distroless/cc-debian12:nonroot AS release | ||
COPY --from=builder /build/target/release/secrets-vault /bin/secrets-vault | ||
ENTRYPOINT ["/bin/secrets-vault"] |
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,125 @@ | ||
# 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.v1alpha1.<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.v1alpha1.<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 derived from the claims attached to the `SecretRequest`. | ||
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 secrets engine mount path and role name to be used for fetching the secrets can be configured in the `policies` entry associated with the given secret. | ||
* See the [Referencing Secrets](#referencing-secrets) section below for an example. | ||
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. | ||
|
||
## How to use it | ||
|
||
### Install with Helm | ||
|
||
If you are looking to run Secrets Vault Server as part of an existing Kubernetes-based deployment, you can easily deploy it using the bundled [Helm chart][helm-chart] using the following command: | ||
|
||
```shell | ||
helm install wasmcloud-secrets-vault oci://ghcr.io/wasmcloud/charts/secrets-vault | ||
|
||
For detailed information on the available configuration options, please see the [Helm chart README][helm-chart]. | ||
|
||
### Configuring Vault | ||
|
||
In order for Secrets Vault Server to work as intended, it will need the following to be pre-configured on the Vault Server it's talking to: | ||
1. Vault Server will need to an instance of the [JWT auth method enabled][vault-jwt-auth-enabled] specifically for the Secrets Vault Server. | ||
2. The JWT auth method will need to be configured to with the [`jwks_url`][vault-jwks-url] configured to point at the JWKS endpoint exposed by the Secrets Vault Service (configured via `--jwks-address` flag `SV_JWKS_ADDRESS` environment variable). Optionally you can also include a default_role for the backend | ||
An example of configuring the above steps might look like this: | ||
```shell | ||
# Enable jwt auth method at provided path | ||
$ vault auth enable -path=jwt jwt | ||
# Configure jwt auth to point it's jwks_url at the JWKS endpoint provided by the Secrets Vault Server | ||
# Please note that the Secrets Vault Server needs to be running in order for Vault to verify the endpoint. | ||
$ vault write auth/jwt/config jwks_url="http://localhost:3000/.well-known/keys" | ||
|
||
# Create a named role with configuration from demo-role.json, see below for example. | ||
$ vault write auth/jwt/role/demo-role @demo-role.json | ||
``` | ||
|
||
Example `demo-json.role`: | ||
|
||
```json | ||
{ | ||
"role_type": "jwt", | ||
"policies": ["demo-role-policy"], | ||
"bound_audiences": "Vault", | ||
"bound_claims": { | ||
"application": ["rust-hello-world", "rust-http-kv", "tinygo-hello-world"] | ||
}, | ||
"user_claim": "sub" | ||
} | ||
``` | ||
|
||
Once you have enabled the jwt auth method and created a named role, you will also need to create the `demo-role-policy` policy: | ||
|
||
```shell | ||
# Create the policy named demo-role-policy with the contents from stdin: | ||
$ vault policy write demo-role-policy - << EOF | ||
# Dev servers have version 2 of KV secrets engine mounted by default, so will | ||
# need this path to grant permissions: | ||
path "secret/data/*" { | ||
capabilities = ["create", "update", "read"] | ||
} | ||
EOF | ||
``` | ||
|
||
### Referencing Secrets | ||
|
||
With the role created and configured, you can now reference any secrets you write on the default `secret` path. | ||
|
||
To configure a wadm application to use policies, you will need to add `policies` section in your wadm manifest: | ||
|
||
```yaml | ||
# ... beginning of the manifest ... | ||
spec: | ||
policies: | ||
- name: vault-secrets-example | ||
type: policy.secret.wasmcloud.dev/v1alpha1 | ||
properties: | ||
backend: 'vault' | ||
role_name: 'demo-role' | ||
mount_path: 'jwt' | ||
# ... rest of the manifest ... | ||
``` | ||
|
||
And then in your component or provider's `properties` section you will need the a `secrets` section that references the policy: | ||
|
||
```yaml | ||
# ... rest of the component or provider definition ... | ||
secrets: | ||
- name: 'secret-name-in-your-code' | ||
source: | ||
policy: 'vault-secrets-example' | ||
key: 'path/to/secret/in/vault' | ||
# ... rest of the manifest ... | ||
``` | ||
|
||
[helm-chart]: https://github.com/wasmCloud/wasmCloud-contrib/blob/main/secrets/secrets-vault/charts/secrets-vault/README.md | ||
[vault-bound-claims]: https://developer.hashicorp.com/vault/docs/auth/jwt#bound-claims | ||
[vault-jwks-url]: https://developer.hashicorp.com/vault/api-docs/auth/jwt#jwks_url | ||
[vault-jwt-auth]: https://developer.hashicorp.com/vault/docs/auth/jwt#jwt-authentication | ||
[vault-jwt-auth-enabled]: https://developer.hashicorp.com/vault/docs/auth/jwt#configuration | ||
[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 |
Oops, something went wrong.