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
+2 -1
View File
@@ -245,6 +245,7 @@ var (
rbac.ResourceWorkspaceAgentDevcontainers.Type: {policy.ActionCreate},
// Provisionerd creates usage events
rbac.ResourceUsageEvent.Type: {policy.ActionCreate},
rbac.ResourceUserSecret.Type: {policy.ActionRead},
}),
User: []rbac.Permission{},
ByOrgID: map[string]rbac.OrgPermissions{},
@@ -5821,7 +5822,7 @@ func (q *querier) ListUserSecretsWithValues(ctx context.Context, userID uuid.UUI
// This query returns decrypted secret values and must only be called
// from system contexts (provisioner, agent manifest). REST API
// handlers should use ListUserSecrets (metadata only).
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil {
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceUserSecret); err != nil {
return nil, err
}
return q.db.ListUserSecretsWithValues(ctx, userID)
+1 -1
View File
@@ -5667,7 +5667,7 @@ func (s *MethodTestSuite) TestUserSecrets() {
secret := testutil.Fake(s.T(), faker, database.UserSecret{UserID: user.ID})
dbm.EXPECT().ListUserSecretsWithValues(gomock.Any(), user.ID).Return([]database.UserSecret{secret}, nil).AnyTimes()
check.Args(user.ID).
Asserts(rbac.ResourceSystem, policy.ActionRead).
Asserts(rbac.ResourceUserSecret, policy.ActionRead).
Returns([]database.UserSecret{secret})
}))
s.Run("CreateUserSecret", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
+7 -3
View File
@@ -1590,8 +1590,8 @@ func PresetParameter(t testing.TB, db database.Store, seed database.InsertPreset
return parameters
}
func UserSecret(t testing.TB, db database.Store, seed database.UserSecret) database.UserSecret {
userSecret, err := db.CreateUserSecret(genCtx, database.CreateUserSecretParams{
func UserSecret(t testing.TB, db database.Store, seed database.UserSecret, mutators ...func(params *database.CreateUserSecretParams)) database.UserSecret {
params := database.CreateUserSecretParams{
ID: takeFirst(seed.ID, uuid.New()),
UserID: takeFirst(seed.UserID, uuid.New()),
Name: takeFirst(seed.Name, "secret-name"),
@@ -1600,7 +1600,11 @@ func UserSecret(t testing.TB, db database.Store, seed database.UserSecret) datab
ValueKeyID: seed.ValueKeyID,
EnvName: takeFirst(seed.EnvName, "SECRET_ENV_NAME"),
FilePath: takeFirst(seed.FilePath, "~/secret/file/path"),
})
}
for _, mut := range mutators {
mut(&params)
}
userSecret, err := db.CreateUserSecret(genCtx, params)
require.NoError(t, err, "failed to insert user secret")
return userSecret
}