-
Notifications
You must be signed in to change notification settings - Fork 152
Add support for Basic Auth through AuthenticationFilter #4436
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
base: feat/authentication-filter-basic-auth
Are you sure you want to change the base?
Conversation
| NGFPolicies map[PolicyKey]policies.Policy | ||
| SnippetsFilters map[types.NamespacedName]*ngfAPIv1alpha1.SnippetsFilter | ||
| AuthenticationFilters map[types.NamespacedName]*ngfAPIv1alpha1.AuthenticationFilter |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had through about creating a map similar to NGCPolicies for our filter. I'm not 100% sure if it would make sense though and it will require a bit of re-wiring. Would love to know what every thinks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You mean to combine Snippets and Auth into one map for filters? It may be possible, though they do behave differently.
The generic Policy type is used because all of our policies are treated essentially the same in terms of how we process and render them in the nginx conf. Snippets are a bit of a special case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, that's good to know. Thanks Saylor!
With that in mind lets avoid that change as part of this PR. It can be something we come back and consider in the future if we find we're making more filter CRDs
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR implements support for Basic Authentication through a new AuthenticationFilter CRD. Routes (both HTTPRoute and GRPCRoute) can now reference an AuthenticationFilter to require Basic Auth credentials on requests. The implementation includes validation of the filter configuration, secret references, and NGINX configuration generation to enforce authentication at the location level.
Key Changes
- Introduces the
AuthenticationFilterCRD for configuring Basic Auth via secret references - Extends HTTPRoute and GRPCRoute processing to support the new filter type
- Adds NGINX configuration generation for
auth_basicdirectives and user file management
Reviewed changes
Copilot reviewed 30 out of 30 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| apis/v1alpha1/authenticationfilter_types.go | Adds constant for Basic Auth secret key |
| apis/v1alpha1/register.go | Registers AuthenticationFilter types with the scheme |
| internal/framework/kinds/kinds.go | Adds AuthenticationFilter kind constant |
| internal/controller/state/graph/authentication_filter.go | Implements validation and processing logic for AuthenticationFilters |
| internal/controller/state/graph/secret.go | Updates secret validation to accept Opaque secrets with "auth" key |
| internal/controller/state/graph/extension_ref_filter.go | Adds AuthenticationFilter support to extension ref validation |
| internal/controller/state/graph/common_filter.go | Updates filter resolution to use map-based resolver lookup |
| internal/controller/state/graph/httproute.go | Threads AuthenticationFilter support through HTTP route processing |
| internal/controller/state/graph/grpcroute.go | Threads AuthenticationFilter support through GRPC route processing |
| internal/controller/state/graph/route_common.go | Updates route building to accept AuthenticationFilters |
| internal/controller/state/graph/graph.go | Adds AuthenticationFilters to cluster state and graph |
| internal/controller/state/dataplane/types.go | Defines AuthenticationFilter and BasicAuth dataplane types |
| internal/controller/state/dataplane/convert.go | Implements conversion from graph to dataplane representation |
| internal/controller/state/dataplane/configuration.go | Threads authentication filters through configuration building |
| internal/controller/state/conditions/conditions.go | Adds condition constructors for AuthenticationFilter status |
| internal/controller/state/status/status_setters.go | Implements status setter for AuthenticationFilter |
| internal/controller/state/status/prepare_requests.go | Prepares status update requests for AuthenticationFilters |
| internal/controller/state/change_processor.go | Adds AuthenticationFilter to change processing |
| internal/controller/handler.go | Updates status handling to include AuthenticationFilters |
| internal/controller/manager.go | Registers AuthenticationFilter controller and initial sync |
| internal/controller/nginx/config/servers.go | Implements NGINX configuration for Basic Auth |
| internal/controller/nginx/config/servers_template.go | Adds template directives for auth_basic |
| internal/controller/nginx/config/http/config.go | Adds AuthBasic configuration types |
| internal/controller/nginx/config/generator.go | Generates auth user files for Basic Auth |
| charts/nginx-gateway-fabric/templates/clusterrole.yaml | Adds RBAC permissions for AuthenticationFilter resources |
| Test files | Updates tests to pass nil for new AuthenticationFilter parameters |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // TODO: Define our own Secret types for other auth methods (e.g., OAuth) as needed. | ||
| case secret.Type == apiv1.SecretTypeOpaque: | ||
| // Allow Opaque secrets specifically when they contain the "auth" key. | ||
| //nolint:revive // may need to consider our own secret types. |
Copilot
AI
Dec 10, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The inline comment 'may need to consider our own secret types' doesn't clearly explain why the revive lint is being suppressed. Consider updating this to be more specific about what revive is flagging and why it's acceptable in this context.
| //nolint:revive // may need to consider our own secret types. | |
| //nolint:revive // Suppress revive warning for empty branch: intentionally allow Opaque secrets with "auth" key for future support of custom secret types (e.g., OAuth). No action needed in this branch. |
| // Will need to test how this behaves if we don't get a match. | ||
| extRefFilterResolver := extRefFilterResolvers[string(f.ExtensionRef.Kind)] |
Copilot
AI
Dec 10, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This TODO-style comment should be addressed before merging. If this is a known edge case that needs handling, add explicit error handling or validation. If testing has already confirmed the behavior is acceptable, remove the comment.
| // Will need to test how this behaves if we don't get a match. | |
| extRefFilterResolver := extRefFilterResolvers[string(f.ExtensionRef.Kind)] | |
| extRefFilterResolver, ok := extRefFilterResolvers[string(f.ExtensionRef.Kind)] | |
| if !ok { | |
| err := field.NotFound(filterPath.Child("extensionRef.kind"), f.ExtensionRef.Kind) | |
| errors.resolve = append(errors.resolve, err) | |
| valid = false | |
| continue | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I put quite a few TODO comments in this PR. I'll come back to address and/or remove them after the review.
| { | ||
| gvk: cfg.MustExtractGVK(&ngfAPIv1alpha1.AuthenticationFilter{}), | ||
| store: newObjectStoreMapAdapter(clusterStore.AuthenticationFilters), | ||
| predicate: nil, // Not sure if this should be nil or if we should track references |
Copilot
AI
Dec 10, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comment indicates uncertainty about the implementation. The predicate should be nil if you want to write status to all AuthenticationFilters (like SnippetsFilters), or it should filter based on references if you only want to update referenced ones. This should be resolved before merging.
| predicate: nil, // Not sure if this should be nil or if we should track references | |
| predicate: nil, // we always want to write status to AuthenticationFilters so we don't filter them out |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For SnippetsFilter, we mention that "we always want to write status to SnippetsFilters so we don't filter them out". Would the same apply for AuthenticationFilter? I con't think of scenarios where we wouldn't want to write the status.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, probably.
| logger := ctlrZap.New().WithName("update-location-auth-filter") | ||
|
|
||
| // TODO: Remove logging after debugging | ||
| if authenticationFilter == nil { | ||
| logger.Info("Missing AuthenticationFilter for location", "locationPath", location.Path) | ||
| } else if authenticationFilter.Basic == nil { | ||
| logger.Info("No Basic authentication configured for location", "locationPath", location.Path) | ||
| } | ||
|
|
||
| if authenticationFilter != nil { | ||
| logger.Info("Applying authentication filter to location", "locationPath", location.Path) |
Copilot
AI
Dec 10, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function contains temporary debug logging that should be removed before merging (as noted by the TODO comment). Remove the logger initialization and all log statements on lines 730-740, 750-753.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
| ) | ||
|
|
||
| const ( | ||
| AuthKeyBasic = "auth" // Key in the Secret data for Basic Auth credentials. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's add a properly formatted comment above the field. And why "Basic" in the name?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good. I chose the word AuthKeyBasic so we can distinguish between the key for JWT when we introduce that. So that new key might be set to `AuthKeyJWT = "jwks.json"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I though we decided we were just going to use auth as the generic field name for both Basic and JWT?
The JWT is going to be base64 encoded anyway, and technically not a JSON format in the Secret itself.
| type AuthBasic struct { | ||
| Realm string | ||
| Data AuthBasicData | ||
| } | ||
|
|
||
| type AuthBasicData struct { | ||
| FileName string | ||
| FileData []byte | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will need comments.
| // nginxPlusConfigFile is the path to the file containing the NGINX Plus API config. | ||
| nginxPlusConfigFile = httpFolder + "/plus-api.conf" | ||
|
|
||
| basicAuthUserFile = configFolder + "/secrets/%s" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
secretsFolder is already defined above
Also, we shouldn't need a global var for the user file name, it can just be generated on the fly as needed.
| return location | ||
| } | ||
|
|
||
| func createExecuteResultsForAuthBasicUserFile(servers []http.Server) []executeResult { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we not follow the pattern that we use in generateCertBundle to create the secret files? (related to my other comment as well about that function)
|
|
||
| type AuthBasicData struct { | ||
| FileName string | ||
| FileData []byte |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The file data isn't needed on the location block. I wonder if we can structure this better. For example, the way we currently process TLS secret files. The server sets the file path, but the data isn't stored on the server Go struct at all. It's a separate field in the dataplane conf. See how we handle generateCertBundle.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @sjberman ! This is exactly the kind of thing I wanted to find in this review. 😄
I'll take a dig into that part of the code and see if we can simplify this.
| allErrs = append(allErrs, valErr) | ||
| } | ||
|
|
||
| resolvedSecrets := secretResolver.getResolvedSecrets() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
getResolvedSecrets() is now being called twice when we build the graph. Let's just call it once to save on overhead.
| r.resolvedSecrets[nsname] = &secretEntry{ | ||
| Secret: Secret{ | ||
| Source: secret, | ||
| CertBundle: certBundle, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't really make sense in the context of a basic auth secret.
| if secret == nil { | ||
| allErrs = append(allErrs, field.Invalid(field.NewPath("spec.basic.secretRef"), af.Spec.Basic.SecretRef.Name, msg)) | ||
| break | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Didn't we already verify this and add an error after we called resolve?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed here
| if secret.Source == nil { | ||
| allErrs = append(allErrs, field.Invalid(field.NewPath("spec.basic.secretRef"), af.Spec.Basic.SecretRef.Name, msg)) | ||
| break | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't this an internal coding error if this value is nil?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed here
| if _, exists := secret.Source.Data[ngfAPI.AuthKeyBasic]; !exists { | ||
| msg = "referenced secret does not contain required 'auth' key" | ||
| allErrs = append(allErrs, field.Invalid(field.NewPath("spec.basic.secretRef"), af.Spec.Basic.SecretRef.Name, msg)) | ||
| } | ||
| break |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similarly, doesn't resolve already verify this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Had to look over it a few times but I think you're right. With the resolve function, we already know if we have a secret with the right type and key.
We also know if it exists or not, making the first part of the logic redundant.
Nice catch with that. I'll re-run a some checks and make sure everything still works.
Proposed changes
This change adds support for Basic Authentication using the new AuthenticationFilter CRD.
For both HTTPRoute and GRPCRoute resource, requests made to route rules that reference an
AuthenticationFilterusingtype: Basicwill require credentials sent as part of the request.Example HTTP requests:
Example GRPC requests:
Closes #4312
Checklist
Before creating a PR, run through this checklist and mark each as complete.
Release notes
If this PR introduces a change that affects users and needs to be mentioned in the release notes,
please add a brief note that summarizes the change.