Skip to content

feat(wash): workload env/config/secrets#5117

Merged
ricochet merged 6 commits into
mainfrom
env-secrets
May 21, 2026
Merged

feat(wash): workload env/config/secrets#5117
ricochet merged 6 commits into
mainfrom
env-secrets

Conversation

@ricochet

@ricochet ricochet commented May 4, 2026

Copy link
Copy Markdown
Contributor

Adds a workload: section to .wash/config.yaml with environment
(inline + configFrom + secretFrom), opaque config, and outbound
allowedHosts. Top-level configs: and secrets: carry named
sources of three kinds: inline, file: (.env format), and
fromEnv:. Field shape mirrors WorkloadDeployment.localResources
so the same file can round-trip to a Kubernetes manifest later.

Resolution lives in crates/wash/src/workload.rs. Secrets get a
stricter posture: file paths canonicalized and confined to the
project dir (CVE-2025-62725 class), 0600/0400 mode required on
Unix, gitignore-aware warning when a secret file resolves inside
the repo working tree. Errors and traces reference sources by name
only - resolved values never appear in log output.

wash dev now resolves the workload before deploy and applies the
result to the dev component's LocalResources. The wasi:config
plugin is constructed with copy_environment=true, so workload env
vars surface via both wasi:cli/env and wasi:config/store::get.
workload.config is injected into the wasi:config interface entry
when at least one component in the workload imports it, sidecars
included.

create_workload is split into a thin I/O wrapper plus a pure
build_workload that the unit tests exercise end-to-end. Also fixes
a pre-existing gap by honoring dev.component_path (lets the dev
build command produce its artifact in a different folder than the
release build).

Fixes #5088
Fixes #5099

@ricochet ricochet requested a review from a team as a code owner May 4, 2026 19:42
@ricochet ricochet force-pushed the env-secrets branch 3 times, most recently from 9175614 to 7339efb Compare May 6, 2026 15:34
ricochet added a commit that referenced this pull request May 7, 2026
Builds on #5117 to make this
example a complete walkthrough of the new `.wash/config.yaml`
schema: an `app-defaults` config-source feeds non-sensitive runtime
knobs from `config/defaults.env`, an `upstream-credentials`
secret-source pulls `UPSTREAM_API_TOKEN` from the developer's shell
via `fromEnv`, `workload.environment.{configFrom,secretFrom}`
references both, and `workload.allowedHosts` constrains the outbound
call. `workload.config` continues to carry the `otel.resource.*`
identity attributes.

- New cached `AppConfig` reads `OUTBOUND_HOST`, `request_timeout_ms`,
  and `UPSTREAM_API_TOKEN` from `wasi:config/store::get_all` once on
  the first request. Defaults (`example.com`, 5s) apply when keys
  are absent.
- `make_outgoing_request` uses the configured URL, applies the
  configured timeout to first-byte and between-bytes, and attaches
  `Authorization: Bearer <token>` when the secret is present.
  Missing-token paths still work so the example is exercisable
  without setting `UPSTREAM_API_TOKEN`.
- New `log_runtime_config_once` emits a single OTel log line at
  startup listing every `wasi:config` key visible to the component.
  Keys only since secret values land in this same map under
  `copy_environment = true` and the schema's contract is that values
  are never logged.
- Outgoing HTTP span name is now low-cardinality `"outgoing http"`;
  the host moves to the `url.full` attribute.

Signed-off-by: Bailey Hayes <bailey@cosmonic.com>
@ricochet ricochet force-pushed the env-secrets branch 2 times, most recently from 2e83733 to 045bb71 Compare May 7, 2026 02:30
@ricochet ricochet requested a review from vados-cosmonic May 13, 2026 20:58

@vados-cosmonic vados-cosmonic left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did a quick round of review -- I personally am OK with this landing and us iterating on it rather than being stuck in review too long!

In particular I think we can iterate a bit if we land this under flag for early experimentation (but honestly maybe that's too much extra ceremony).

Also, I kind of like the idea of workload::config::X and workload::secrets::Y for interacting with different things related to workloads

Comment thread crates/wash-runtime/src/plugin/wasi_otel/mod.rs
Comment thread crates/wash-runtime/src/plugin/wasi_otel/mod.rs Outdated
Comment thread crates/wash-runtime/src/plugin/wasi_otel/mod.rs
Comment thread crates/wash-runtime/src/plugin/wasi_otel/mod.rs
Comment thread crates/wash/src/config.rs Outdated
Comment thread crates/wash/src/workload.rs Outdated
Comment thread crates/wash/src/workload.rs Outdated
Comment thread crates/wash/src/cli/dev.rs Outdated
Comment thread crates/wash/src/cli/dev.rs
Comment thread crates/wash/src/cli/dev.rs Outdated
@ricochet ricochet requested a review from a team as a code owner May 18, 2026 19:43
@ricochet ricochet force-pushed the env-secrets branch 2 times, most recently from e4238a7 to f980925 Compare May 18, 2026 20:17
jfleitz
jfleitz previously approved these changes May 18, 2026
@ricochet ricochet force-pushed the env-secrets branch 2 times, most recently from c3cc5ff to 9f38978 Compare May 19, 2026 01:21
ricochet added a commit that referenced this pull request May 19, 2026
Builds on #5117 to make this
example a complete walkthrough of the new `.wash/config.yaml`
schema: an `app-defaults` config-source feeds non-sensitive runtime
knobs from `config/defaults.env`, an `upstream-credentials`
secret-source pulls `UPSTREAM_API_TOKEN` from the developer's shell
via `fromEnv`, `workload.environment.{configFrom,secretFrom}`
references both, and `workload.allowedHosts` constrains the outbound
call. `workload.config` continues to carry the `otel.resource.*`
identity attributes.

- New cached `AppConfig` reads `OUTBOUND_HOST`, `request_timeout_ms`,
  and `UPSTREAM_API_TOKEN` from `wasi:config/store::get_all` once on
  the first request. Defaults (`example.com`, 5s) apply when keys
  are absent.
- `make_outgoing_request` uses the configured URL, applies the
  configured timeout to first-byte and between-bytes, and attaches
  `Authorization: Bearer <token>` when the secret is present.
  Missing-token paths still work so the example is exercisable
  without setting `UPSTREAM_API_TOKEN`.
- New `log_runtime_config_once` emits a single OTel log line at
  startup listing every `wasi:config` key visible to the component.
  Keys only since secret values land in this same map under
  `copy_environment = true` and the schema's contract is that values
  are never logged.
- Outgoing HTTP span name is now low-cardinality `"outgoing http"`;
  the host moves to the `url.full` attribute.

Signed-off-by: Bailey Hayes <bailey@cosmonic.com>
ricochet added a commit that referenced this pull request May 19, 2026
Builds on #5117 to make this
example a complete walkthrough of the new `.wash/config.yaml`
schema: an `app-defaults` config-source feeds non-sensitive runtime
knobs from `config/defaults.env`, an `upstream-credentials`
secret-source pulls `UPSTREAM_API_TOKEN` from the developer's shell
via `fromEnv`, `workload.environment.{configFrom,secretFrom}`
references both, and `workload.allowedHosts` constrains the outbound
call. `workload.config` continues to carry the `otel.resource.*`
identity attributes.

- New cached `AppConfig` reads `OUTBOUND_HOST`, `request_timeout_ms`,
  and `UPSTREAM_API_TOKEN` from `wasi:config/store::get_all` once on
  the first request. Defaults (`example.com`, 5s) apply when keys
  are absent.
- `make_outgoing_request` uses the configured URL, applies the
  configured timeout to first-byte and between-bytes, and attaches
  `Authorization: Bearer <token>` when the secret is present.
  Missing-token paths still work so the example is exercisable
  without setting `UPSTREAM_API_TOKEN`.
- New `log_runtime_config_once` emits a single OTel log line at
  startup listing every `wasi:config` key visible to the component.
  Keys only since secret values land in this same map under
  `copy_environment = true` and the schema's contract is that values
  are never logged.
- Outgoing HTTP span name is now low-cardinality `"outgoing http"`;
  the host moves to the `url.full` attribute.

Signed-off-by: Bailey Hayes <bailey@cosmonic.com>
@vados-cosmonic vados-cosmonic self-requested a review May 19, 2026 15:06
vados-cosmonic
vados-cosmonic previously approved these changes May 19, 2026
ricochet added 3 commits May 20, 2026 20:49
Adds a `workload:` section to .wash/config.yaml with `environment`
(inline + configFrom + secretFrom), opaque `config`, and outbound
`allowedHosts`. Top-level `configs:` and `secrets:` carry named
sources of three kinds: `inline`, `file:` (.env format), and
`fromEnv:`. Field shape mirrors WorkloadDeployment.localResources
so the same file can round-trip to a Kubernetes manifest later.

Resolution lives in `crates/wash/src/workload.rs`. Secrets get a
stricter posture: file paths canonicalized and confined to the
project dir (CVE-2025-62725 class), 0600/0400 mode required on
Unix, gitignore-aware warning when a secret file resolves inside
the repo working tree. Errors and traces reference sources by name
only - resolved values never appear in log output.

`wash dev` now resolves the workload before deploy and applies the
result to the dev component's LocalResources. The wasi:config
plugin is constructed with copy_environment=true, so workload env
vars surface via both wasi:cli/env and wasi:config/store::get.
`workload.config` is injected into the wasi:config interface entry
when at least one component in the workload imports it, sidecars
included.

create_workload is split into a thin I/O wrapper plus a pure
build_workload that the unit tests exercise end-to-end. Also fixes
a pre-existing gap by honoring `dev.component_path` (lets the dev
build command produce its artifact in a different folder than the
release build).

Fixes #5088
Fixes #5099

Signed-off-by: Bailey Hayes <bailey@cosmonic.com>
Add a `dev.environment` map that wash dev exports into its own process
before constructing the host, so plugins and runtime crates that read
from std::env (RUST_LOG, OTEL_*, libpq's PG*, ...) pick the values up.
Distinct from workload.environment, which is delivered to the component
via wasi:cli/env.

It is awkward that we have to add it very early in the main process,
since otel is initialized before we have a chance to read the config.

Also add the first schema tests for the Config struct, pinning the
camelCase contract on WorkloadConfig / EnvironmentLayer / ConfigSource
so a future refactor that drops a `rename_all` doesn't silently drop
user-configured fields.

Signed-off-by: Bailey Hayes <bailey@cosmonic.com>
Replace `Vec<String>` / `Arc<[String]>` for `allowed_hosts` with a typed
`AllowedHost` enum so the runtime hot path matches against parsed
variants. The wire format stays as `[String]` on both wash YAML and the
proto API; parsing happens once at the conversion boundary.

The enum has four variants: `Any` (`*`), `Authority(host[:port])`,
`Url(scheme://host[:port][/])`, and `SuffixWildcard(*.host)`. Wildcards
require a leading dot (`*.foo`, not `*foo`) so `*com` matching every
.com is a parse error. URL form rejects paths beyond bare `/`, query
strings, and fragments `allowed_hosts` is a host policy, not a URL
policy.

Runtime semantics change to fail-closed: empty `allowed_hosts` now
denies all outgoing requests (was: allow all). The wash config layer
substitutes `[Any]` when `allowedHosts` is omitted from YAML via a serde
default, so `wash dev` stays ergonomic for users who haven't thought
about egress. Explicit `allowedHosts: []` in YAML stays empty and
denies. `DevRouter` now calls `check_allowed_hosts` instead of
shortcutting so the same policy is applied across both routers.

Split `ConfigSource` and `SecretSource` into distinct types with
`resolve` methods so the stricter secret-handling posture (Unix mode
`0600`/`0400`, `O_NOFOLLOW` open + `fstat` perm check, in-repo-tree
warning) can only be applied to secrets. Drops the `secret: bool`
parameter from internal resolution helpers.

Secret-file open is now TOCTOU-safe via `O_NOFOLLOW` (final-component
symlink swap rejected) + `File::metadata()` (`fstat` on Unix, can't be
raced by path-based mode swap between check and read).

DynamicConfig and WasiOtelConfig adopt `bon::Builder` for
forward-compatible construction.

Signed-off-by: Bailey Hayes <bailey@cosmonic.com>
ricochet added 3 commits May 20, 2026 20:49
Annotate the AllowedHosts field on LocalResources with a regex matching
the four accepted forms (`*`, `host[:port]`, `scheme://host[:port][/]`,
`*.suffix[:port]`, `scheme://*.suffix[:port][/]`) so K8s admission
rejects bad entries before they reach the runtime. Document that empty
or absent allowedHosts denies all outgoing requests (fail-closed),
matching the runtime's new behavior; users opt into allow-all with
`allowedHosts: ["*"]`. Final validation still runs in the runtime.
This regex is an admission-time guard, not the source of truth.

Regenerated CRDs via `make manifests`.

Signed-off-by: Bailey Hayes <bailey@cosmonic.com>
- `integration_http_allowed_hosts`: cover the four AllowedHost shapes
  end-to-end. Add `test_star_any_permits_all` (literal `*` allows
  everything), `test_url_policy_pins_scheme` (URL form rejects wrong
  scheme), and flip `test_empty_allowed_hosts_permits_all` →
  `test_empty_allowed_hosts_denies_all`.
- `integration_http_counter` / `integration_http_tls` / `tests/common`:
  populate `LocalResources.allowed_hosts` with `["example.com"]` since
  the http-counter fixture makes outgoing calls to example.com and the
  empty-list default now denies. Encoding the actual target host
  documents the test's intent precisely.

Signed-off-by: Bailey Hayes <bailey@cosmonic.com>
The fixture now reports the host's policy decision via its own status
(200 allowed, 403 denied, 502 connect/DNS error) rather than forwarding
the remote server's status. Forwarding made policy denials look
identical to remote 4xx responses — wikipedia returning 403 to anonymous
clients was failing `test_star_any_permits_all` whenever egress worked.

Swap the test targets to IANA-reserved `example.com`, `example.org`, and
`www.example.com` so the three match outcomes (exact, unrelated, subdomain)
each have a dedicated route and no third-party rate limits or bot
detection are in the loop.

Signed-off-by: Bailey Hayes <bailey@cosmonic.com>
@ricochet ricochet merged commit f7f97ad into main May 21, 2026
57 checks passed
@ricochet ricochet deleted the env-secrets branch May 21, 2026 22:02
ricochet added a commit that referenced this pull request May 22, 2026
Builds on #5117 to make this
example a complete walkthrough of the new `.wash/config.yaml`
schema: an `app-defaults` config-source feeds non-sensitive runtime
knobs from `config/defaults.env`, an `upstream-credentials`
secret-source pulls `UPSTREAM_API_TOKEN` from the developer's shell
via `fromEnv`, `workload.environment.{configFrom,secretFrom}`
references both, and `workload.allowedHosts` constrains the outbound
call. `workload.config` continues to carry the `otel.resource.*`
identity attributes.

- New cached `AppConfig` reads `OUTBOUND_HOST`, `request_timeout_ms`,
  and `UPSTREAM_API_TOKEN` from `wasi:config/store::get_all` once on
  the first request. Defaults (`example.com`, 5s) apply when keys
  are absent.
- `make_outgoing_request` uses the configured URL, applies the
  configured timeout to first-byte and between-bytes, and attaches
  `Authorization: Bearer <token>` when the secret is present.
  Missing-token paths still work so the example is exercisable
  without setting `UPSTREAM_API_TOKEN`.
- New `log_runtime_config_once` emits a single OTel log line at
  startup listing every `wasi:config` key visible to the component.
  Keys only since secret values land in this same map under
  `copy_environment = true` and the schema's contract is that values
  are never logged.
- Outgoing HTTP span name is now low-cardinality `"outgoing http"`;
  the host moves to the `url.full` attribute.

Signed-off-by: Bailey Hayes <bailey@cosmonic.com>
ricochet added a commit that referenced this pull request May 22, 2026
Builds on #5117 to make this
example a complete walkthrough of the new `.wash/config.yaml`
schema: an `app-defaults` config-source feeds non-sensitive runtime
knobs from `config/defaults.env`, an `upstream-credentials`
secret-source pulls `UPSTREAM_API_TOKEN` from the developer's shell
via `fromEnv`, `workload.environment.{configFrom,secretFrom}`
references both, and `workload.allowedHosts` constrains the outbound
call. `workload.config` continues to carry the `otel.resource.*`
identity attributes.

- New cached `AppConfig` reads `OUTBOUND_HOST`, `request_timeout_ms`,
  and `UPSTREAM_API_TOKEN` from `wasi:config/store::get_all` once on
  the first request. Defaults (`example.com`, 5s) apply when keys
  are absent.
- `make_outgoing_request` uses the configured URL, applies the
  configured timeout to first-byte and between-bytes, and attaches
  `Authorization: Bearer <token>` when the secret is present.
  Missing-token paths still work so the example is exercisable
  without setting `UPSTREAM_API_TOKEN`.
- New `log_runtime_config_once` emits a single OTel log line at
  startup listing every `wasi:config` key visible to the component.
  Keys only since secret values land in this same map under
  `copy_environment = true` and the schema's contract is that values
  are never logged.
- Outgoing HTTP span name is now low-cardinality `"outgoing http"`;
  the host moves to the `url.full` attribute.

Signed-off-by: Bailey Hayes <bailey@cosmonic.com>
ricochet added a commit that referenced this pull request May 27, 2026
Builds on #5117 to make this
example a complete walkthrough of the new `.wash/config.yaml`
schema: an `app-defaults` config-source feeds non-sensitive runtime
knobs from `config/defaults.env`, an `upstream-credentials`
secret-source pulls `UPSTREAM_API_TOKEN` from the developer's shell
via `fromEnv`, `workload.environment.{configFrom,secretFrom}`
references both, and `workload.allowedHosts` constrains the outbound
call. `workload.config` continues to carry the `otel.resource.*`
identity attributes.

- New cached `AppConfig` reads `OUTBOUND_HOST`, `request_timeout_ms`,
  and `UPSTREAM_API_TOKEN` from `wasi:config/store::get_all` once on
  the first request. Defaults (`example.com`, 5s) apply when keys
  are absent.
- `make_outgoing_request` uses the configured URL, applies the
  configured timeout to first-byte and between-bytes, and attaches
  `Authorization: Bearer <token>` when the secret is present.
  Missing-token paths still work so the example is exercisable
  without setting `UPSTREAM_API_TOKEN`.
- New `log_runtime_config_once` emits a single OTel log line at
  startup listing every `wasi:config` key visible to the component.
  Keys only since secret values land in this same map under
  `copy_environment = true` and the schema's contract is that values
  are never logged.
- Outgoing HTTP span name is now low-cardinality `"outgoing http"`;
  the host moves to the `url.full` attribute.

Signed-off-by: Bailey Hayes <bailey@cosmonic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ability to configure component_path when using wash dev Add workload-level env, secrets, and config to .wash/config.yaml

3 participants