Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DEVPROD-4507 Redact secrets from OTEL traces and Splunk logs for all graphql requests #7510

Merged
merged 34 commits into from
Feb 20, 2024

Conversation

khelif96
Copy link
Contributor

@khelif96 khelif96 commented Feb 7, 2024

DEVPROD-4507

Description

This PR does several things.

  1. Fixes the OTEL configuration so that we can write traces to honeycomb locally if the API keys and collectors are configured correctly. (I can extract this into a separate pr if desired)
  2. Create a directive called redactSecrets which is used to flag input fields that contain secrets and we should avoid logging to places such as splunk or honeycomb.
  3. Create a script that uses the gqlgen configuration to statically generate a list of fields with the redactSecrets directive. This is necessary because directives are not captured in otel traces. The trace spans begin after the directive middleware has already been run.
  4. Create a function that recursively iterates through a graphql request variables to determine if a known secret field exists and if it does it will swap it out for the word REDACTED. This ensures it will work for all queries of all shapes since the query shape is determined by the client.
  5. Apply the above function to the otelgqlgen tracer to intercept request variables and replace them.
  6. Do the same thing for other places where we log to splunk.
  7. Expose these values as part of a graphql resolver so that we redact secrets that are logged to the client side loggers.

Some limitations

  1. Evaluating the full GraphQL (GQL) type and path for a redacted field is feasible at code generation time, but not during runtime. This limitation stems from the gqlgen library's behavior, where it cannot determine the specific type of a field during query execution. Instead, gqlgen recognizes the field as having a map[string]interface{} type. This interface represents a set of user-defined variables that do not align with the underlying GraphQL structure. Although gqlgen conducts some type evaluation to ensure compatibility, this process is not accessible in a manner that I could utilize. Consequently, the approach I adopted involves identifying the field names designated for redaction and masking any values corresponding to those names, irrespective of their origin within the GraphQL schema.

Testing

Applied the directive to the field taskName
image

image

Splunk logs
image

Documentation

remember to add or edit docs in the docs/ directory if relevant

@khelif96 khelif96 requested review from a team and ybrill February 8, 2024 18:09
@khelif96 khelif96 marked this pull request as ready for review February 8, 2024 18:10
graphql/redact_secrets_plugin.go Show resolved Hide resolved
graphql/splunktracing.go Outdated Show resolved Hide resolved
graphql/http_handler.go Outdated Show resolved Hide resolved
graphql/redact_secrets_plugin.go Outdated Show resolved Hide resolved
graphql/redact_secrets_plugin.go Outdated Show resolved Hide resolved
graphql/redact_secrets_plugin.go Outdated Show resolved Hide resolved
graphql/http_handler.go Outdated Show resolved Hide resolved
Copy link
Contributor

@SupaJoon SupaJoon left a comment

Choose a reason for hiding this comment

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

Can you add an integration test for the redacted field query? The returned value should include the name the of the GQL type and the full path to the redacted value.


// RedactFieldsInMap recursively searches for and redacts fields in a map.
// Assumes map structure like map[string]interface{} where interface{} can be another map, a slice, or a basic datatype.
func RedactFieldsInMap(data map[string]interface{}) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we write unit tests for this function?

graphql/redact_secrets_plugin.go Show resolved Hide resolved
environment.go Outdated Show resolved Hide resolved
@khelif96
Copy link
Contributor Author

khelif96 commented Feb 14, 2024

Can you add an integration test for the redacted field query? The returned value should include the name the of the GQL type and the full path to the redacted value.

I updated the description to explain why the second part is not possible. But I can certainly add a test for it.

graphql/redact_secrets_plugin.go Show resolved Hide resolved
graphql/http_handler.go Outdated Show resolved Hide resolved
graphql/redact_secrets_plugin.go Outdated Show resolved Hide resolved
graphql/http_handler.go Outdated Show resolved Hide resolved
graphql/schema/types/project_settings.graphql Outdated Show resolved Hide resolved
@khelif96 khelif96 requested a review from ybrill February 16, 2024 16:55
Copy link
Contributor

@julianedwards julianedwards left a comment

Choose a reason for hiding this comment

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

LGTM % small nit

}
err := util.DeepCopy(data, &dataCopy, registeredTypes)

if err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: combine this with L85.

}
}

_, err = file.WriteString("}\n\n")
Copy link
Contributor

Choose a reason for hiding this comment

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

I would have thought to have just one \n

Comment on lines +70 to +73
func isFieldRedacted(fieldName string, fieldsToRedact map[string]bool) bool {
_, ok := fieldsToRedact[fieldName]
return ok
}
Copy link
Contributor

Choose a reason for hiding this comment

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

is there an advantage to checking the field is in the map rather than just checking the value in the map? (since values not in the map will return false as well)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think there isn't an advantage to either since they both accomplish the same effect. Fields not in the map should be false either way.

Comment on lines +896 to +900
if e.settings.Tracer.CollectorAPIKey != "" {
opts = append(opts, otlptracegrpc.WithHeaders(map[string]string{
honeycombCollectorHeader: e.settings.Tracer.CollectorAPIKey,
}))
}
Copy link
Contributor

Choose a reason for hiding this comment

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

maybe we'd also want to do this when useInternalDNS is true? On the other hand, maybe we'd never set the header when it's running in k8s. 🤷

@khelif96 khelif96 merged commit 7017279 into evergreen-ci:main Feb 20, 2024
9 checks passed
@khelif96 khelif96 deleted the DEVPROD-4507 branch February 20, 2024 16:38
khelif96 added a commit to khelif96/evergreen that referenced this pull request Feb 20, 2024
khelif96 added a commit that referenced this pull request Feb 20, 2024
…for all graphql requests (#7510)"

This reverts commit 7017279.
khelif96 added a commit to khelif96/evergreen that referenced this pull request Feb 20, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
6 participants