Handling sensitive values
# Proposal
Users can currently mark CI variables as "masked" in the UI and they will be hidden from the jobs logs. We will implement the same functionality in step runner: https://gitlab.com/gitlab-org/step-runner/-/issues/39+s. It must be a core part of step runner because we must mask logs at the source. But we can do better than that.
Some secrets will be generated during CI and should be marked as "masked" dynamically.
And steps should know if they are working with secret data so they can act accordingly.
## Step outputs can be marked as sensitive
In the step specification an output can be marked as sensitive.
`gen_creds/step.yml`:
```yaml
spec:
inputs: {}
outputs:
creds:
type: string
sensitive: true
---
```
Any value derived from a sensitive value will also be marked sensitive.
`.gitlab-ci.yml`:
```yaml
my_job:
run:
- name: gen_creds
step: ./gen_creds
- name: use_creds
step: ./use_creds
inputs:
wrapped_creds: | # This entire value will be marked sensitive
BEGIN
${{ steps.gen_creds.outputs.creds }}
END
```
This is [how Terraform handles sensitive variables](https://developer.hashicorp.com/terraform/tutorials/configuration-language/sensitive-variables#reference-sensitive-variables).
## Sensitive values can only be given to sensitive inputs
Step inputs can be annotated as sensitive, which allows them to handle sensitive values. If a sensitive value is given to a non-sensitive input it will be an error. This guarantees the step knows which incoming values are sensitive so it can act accordingly.
`use_creds/step.yml`
```yaml
spec:
inputs:
wrapped_creds:
type: string
sensitive: true
not_for_creds:
type: string
---
```
`.gitlab-ci.yml`
```yaml
...
- name: use_creds
step: ./use_creds
inputs:
wrapped_creds: | # This is OK
BEGIN
${{ steps.gen_creds.outputs.creds }}
END
not_for_creds: | # This produces an error
BEGIN
${{ steps.gen_creds.outputs.creds }}
END
```
## Masked variables are marked as sensitive
All masked CI variables are marked as sensitive when creating the initial step context.
`.gitlab-ci.yml`:
```yaml
# The variable CREDENTIALS is masked.
my_job:
run:
- name: use_creds
step: ./use_creds
inputs:
wrapped_creds: | # This entire value will be marked sensitive
BEGIN
${{ job.CREDENTIALS }}
END
```
## Environment variables are also marked as sensitive
Environment variables are also marked as sensitive when they are derived from sensitive values.
```yaml
my_job:
run:
- name: use_creds
step: ./use_creds_env
env:
CREDS: | # This entire variable will be marked sensitive
BEGIN
${{ job.CREDENTIALS }}
END
```
## Sensitive values can be exported to a secret file
Because steps do not declare their exported variables, the spec cannot be used to mark a variable as sensitive. Instead users should write their export to `secret_file` instead of `export_file`. The effect on the environment will be identical except the value exported will be marked sensitive.
`gen_creds/step.yml`
```yaml
---
steps:
- name: export_creds
script: echo CREDS=$(./generate-secret) >> ${{ secrets_file }}
# Global environment now has a sensitive value CREDS.
- name: export_not_creds
script: echo NOT_CREDS=$(./generate-random) >> ${{ export_file }}
# Global environment now has a non-sensitive value NOT_CREDS.
```
# Implementation
## Value storage
The baseline working directory and global environment are stored in `context.go` in the [`Global` struct](https://gitlab.com/gitlab-org/step-runner/-/blob/43c2d2b3eff5c0dad5170135c996650b53abea28/pkg/context/context.go#L15-16). The rest of the context is stored in the [`Step` struct](https://gitlab.com/gitlab-org/step-runner/-/blob/43c2d2b3eff5c0dad5170135c996650b53abea28/pkg/context/context.go#L46-51). The [function `DigObject`](https://gitlab.com/gitlab-org/step-runner/-/blob/43c2d2b3eff5c0dad5170135c996650b53abea28/pkg/internal/expression/value_type.go#L95-113) is used when evaluating expressions to retrieve values from the step context. All values in the context are stored as `structpb.Value` types.
Each value should also store its sensitivity alongside the underlying value. This could be just a struct:
```go
type value struct {
v structpb.Value
sensitive bool
}
```
Then `DigObject` can unwrap each value along the path. If any is marked sensitive, the output value will be marked sensitive.
## Output files
The `internal/output` package can add another file `secret_file` alongside [the existing `output_file` and `export_file`](https://gitlab.com/gitlab-org/step-runner/-/blob/43c2d2b3eff5c0dad5170135c996650b53abea28/pkg/internal/output/output.go#L29-30). When exporting to the global context it will process `secret_file` the same way as `export_file` but marking all values as sensitive.
And when writing outputs to the context they will be matched with their output specification and marked accordingly.
## Step invocation
When [adding inputs](https://gitlab.com/gitlab-org/step-runner/-/blob/43c2d2b3eff5c0dad5170135c996650b53abea28/pkg/runner/runner.go#L116) to step invocation parameters, step runner will just check the value sensitive against the spec input sensitivity.
## Dynamic masking
Whenever a new sensitive value is generated by an expression, it will be passed to the masking system which will remove it from any subsequent logs. It will be important the step is not invoked until all new masked values are received by the masking system, so a channel rendezvous or lock will be required.
epic