Audit successful CI/CD variable value access in GraphQL
What does this MR do?
Emits a streamed audit event, variable_viewed_graphql, recording which
CI/CD variable values a GraphQL query accessed, and for which project or
group.
This replaces the structured-log approach the MR originally took (a
query_analysis.successfully_accessed_vars field in the GraphQL log),
following the discussion from
!240847 (comment 3627156051)
onwards. Two reasons for the change:
- Audience. Structured logs are only visible to instance administrators. Audit events can be streamed to external SIEM destinations.
- Abstraction level. The log approach wrote domain-specific data
into
context[:gl_analysis], which otherwise carries only generic query instrumentation: complexity, depth, and field usage.
A comparable event, variable_viewed_api, already exists for the REST
API. A new event type is added rather than extending that one, because
streaming destinations filter by event name: folding GraphQL access into
variable_viewed_api would change what existing subscribers receive and
make filtering for REST-only access impossible. This event carries an
api_type detail so that the two can be queried together once the REST
event carries it as well, which is part of
https://gitlab.com/gitlab-org/gitlab/-/work_items/628040 (confidential).
How it works
Variable values resolve per node, so accesses are accumulated during execution and audited once per query and variable owner:
EE::Types::Ci::ProjectVariableTyperecords each resolved variable into aGitlab::Ci::Variables::AccessCollectorheld in the query context.EE::Types::Ci::GroupVariableTypeoverrides#variable_scopeso group variables resolve their owner from the group, which is needed because the group type inherits from the project type.Gitlab::Graphql::Tracers::CiVariableAuditTraceremits the audit events fromexecute_multiplex, the only hook that runs after query results are materialized.- Keys are deduplicated per owner, so aliased
valueselections are recorded once. - Hidden variables are recorded separately in
hidden_keys, because their value is withheld.variable_viewed_apiaudits access to hidden variables too, so this keeps the two events consistent while still letting consumers filter on values that were actually returned. - The number of keys per owner is capped (
MAX_KEYS_PER_SCOPE) so a query over many variables cannot produce an unbounded payload. Thetruncateddetail is set only when a key is actually dropped. - Auditing one owner cannot fail the query or suppress the events for the remaining owners: failures are tracked instead of raised, per owner.
- Values read by a query that later fails are still audited, because they were already decrypted in process.
- Gated behind the
audit_ci_variable_value_accessfeature flag (gitlab_com_derisk), renamed fromlog_ci_variable_value_access.
CE and EE
Audit events are an EE feature, and this one is unreachable from CE: the
event type is defined in ee/config, so Gitlab::Audit::Auditor raises
when that definition is not loaded, and the CE gate rejects a Project
or Group scope in any case.
Everything therefore lives in ee/: the collector, the tracer, the
trace_with registration, the feature flag definition, the recording
itself, and the specs. CE keeps only two prepend_mod calls, so
ProjectVariableType#value is unchanged from before this MR. FOSS does
no work for this feature.
Event payload
variable_viewed_graphql is stream-only (saved_to_database: false),
matching variable_viewed_api after
!200323 (merged). It is
delivered only to configured streaming destinations, so it does not
appear in the audit events UI or in audit_json.log.
| Field | Value |
|---|---|
scope / target |
The project or group owning the variables, which yields entity_path |
target_details |
Comma-separated list of the keys whose values were returned, or the auditor's default when only hidden variables were requested |
ip_address |
Populated from the request context |
additional_details.api_type |
graphql |
additional_details.accessed_keys |
Keys whose values were returned |
additional_details.hidden_keys |
Keys requested but withheld because the variable is hidden |
additional_details.truncated |
Whether any key was dropped from the lists above |
A query reading variables from more than one owner produces one event per owner.
Follow-ups
Access token details, tracked in
https://gitlab.com/gitlab-org/gitlab/-/work_items/628040 (confidential).
Recording access_token_id and access_token_name was deferred by
agreement with the Security team. Neither is captured automatically
today, and variable_viewed_api does not record token details either, so
one change should improve both events.
Instance-level CI/CD variables. Types::Ci::InstanceVariableType
also exposes value and is deliberately not covered here. Instance
variables are unaudited on every read path today, including REST
(/admin/ci/variables), GraphQL, and the admin UI, and
variable_viewed_api is scoped to Project and Group only. Covering
only the GraphQL half would be inconsistent, and doing it properly pulls
in Gitlab::Audit::InstanceScope and the admin_audit_log licensing
path. Note that InstanceVariableType inherits from BaseObject, not
ProjectVariableType, so it is not incidentally covered by the recording
added here.
References
https://gitlab.com/gitlab-org/gitlab/-/work_items/602520 (confidential)