feat: plumb user secrets through provisioner chain to terraform (#24542)

This change passes user secrets from coderd to the Terraform process at
workspace build time so the `data.coder_secret` data source in
terraform-provider-coder can resolve values at plan time.

Secrets traverse two proto hops: `provisionerdserver` fetches them
via`ListUserSecretsWithValues`, attaches them to
`AcquiredJob.WorkspaceBuild.user_secrets` on `provisionerd.proto`;
`runner.go` forwards into `PlanRequest.user_secrets` on
`provisioner.proto`; the Terraform provisioner encodes each as
`CODER_SECRET_ENV_<name>` or `CODER_SECRET_FILE_<hex(path)>` before
invoking `terraform plan`. Only plan requests carry secrets; apply runs
with `nil` because values are baked into plan state.

Fetch is gated on a workspace transitioning to start. stop and delete
transitions never carry secrets, so revoking or deleting a stored secret
cannot make a workspace unstoppable. DB errors on the fetch fail the job
outright rather than silently continuing with an empty secret set.

Note that user secrets will be stored in the workspace_builds table in
provisioner_state with other Terraform state (including other sensitive data).
This commit is contained in:
Zach
2026-04-27 08:26:07 -06:00
committed by GitHub
parent 66abd8a271
commit 79735f2d45
15 changed files with 1471 additions and 686 deletions
+54
View File
@@ -477,6 +477,35 @@ export interface InitComplete {
moduleFilesHash: Uint8Array;
}
/**
* UserSecretValue carries a single user secret to a provisioner. env_name and
* file_path describe the bindings the user requested when creating the secret.
* The terraform provisioner exposes secrets via CODER_SECRET_ENV_* and
* CODER_SECRET_FILE_* environment variables consumed by terraform-provider-coder's
* coder_secret data source
*/
export interface UserSecretValue {
/**
* Environment variable name the user selected (e.g. "GITHUB_TOKEN"). Intended
* to be treated as an opaque lookup key, i.e. consumers must preserve it
* verbatim when matching against a data.coder_secret.env_name attribute.
* Consumers can assume names are POSIX-compliant. Optional: env_name and
* file_path are independent.
*/
envName: string;
/**
* Filesystem path the user requested this secret be bound to (e.g. "~/creds"
* or "/etc/creds"). This path is not expanded. Expansion happens only where
* the secret is actually materialized on disk. Intended to be treated as an
* opaque lookup key, i.e. consumers must preserve it verbatim when matching
* against a data.coder_secret.file attribute. Optional; env_name and
* file_path are independent.
*/
filePath: string;
/** Secret value, which may be arbitrary binary data. */
value: Uint8Array;
}
/** PlanRequest asks the provisioner to plan what resources & parameters it will create */
export interface PlanRequest {
metadata: Metadata | undefined;
@@ -486,6 +515,13 @@ export interface PlanRequest {
previousParameterValues: RichParameterValue[];
/** state is the provisioner state (if any) */
state: Uint8Array;
/**
* User secrets to make available during plan. Not carried on ApplyRequest
* because plan evaluates data.coder_secret references and bakes the
* resolved values into plan state, so apply does not need the raw secrets.
* Provisioner-specific handling is documented on the UserSecretValue message.
*/
userSecrets: UserSecretValue[];
}
/** PlanComplete indicates a request to plan completed. */
@@ -1479,6 +1515,21 @@ export const InitComplete = {
},
};
export const UserSecretValue = {
encode(message: UserSecretValue, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
if (message.envName !== "") {
writer.uint32(10).string(message.envName);
}
if (message.filePath !== "") {
writer.uint32(18).string(message.filePath);
}
if (message.value.length !== 0) {
writer.uint32(26).bytes(message.value);
}
return writer;
},
};
export const PlanRequest = {
encode(message: PlanRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
if (message.metadata !== undefined) {
@@ -1499,6 +1550,9 @@ export const PlanRequest = {
if (message.state.length !== 0) {
writer.uint32(50).bytes(message.state);
}
for (const v of message.userSecrets) {
UserSecretValue.encode(v!, writer.uint32(58).fork()).ldelim();
}
return writer;
},
};