Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 2 additions & 16 deletions internal/controller/consumer_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ import (
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
"sigs.k8s.io/gateway-api/apis/v1beta1"

"github.com/apache/apisix-ingress-controller/api/v1alpha1"
"github.com/apache/apisix-ingress-controller/internal/controller/config"
Expand Down Expand Up @@ -255,21 +254,8 @@ func (r *ConsumerReconciler) processSpec(ctx context.Context, tctx *provider.Tra
ns = *credential.SecretRef.Namespace
}
// A cross-namespace SecretRef needs a ReferenceGrant, same as routes.
secretNS := gatewayv1.Namespace(ns)
if permitted := checkReferenceGrant(ctx,
r.Client,
v1beta1.ReferenceGrantFrom{
Group: v1beta1.Group(v1alpha1.GroupVersion.Group),
Kind: v1beta1.Kind(internaltypes.KindConsumer),
Namespace: v1beta1.Namespace(consumer.GetNamespace()),
},
gatewayv1.ObjectReference{
Group: corev1.GroupName,
Kind: KindSecret,
Name: gatewayv1.ObjectName(credential.SecretRef.Name),
Namespace: &secretNS,
},
); !permitted {
secretNN := types.NamespacedName{Namespace: ns, Name: credential.SecretRef.Name}
if !CheckConsumerSecretRef(ctx, r.Client, consumer.GetNamespace(), secretNN) {
r.Log.Error(nil, "cross-namespace secret reference not permitted by any ReferenceGrant",
"consumer", utils.NamespacedName(consumer), "secret", client.ObjectKey{Namespace: ns, Name: credential.SecretRef.Name})
return fmt.Errorf("cross-namespace secret reference from Consumer %s/%s to Secret %s/%s is not permitted by any ReferenceGrant",
Expand Down
19 changes: 19 additions & 0 deletions internal/controller/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -1345,6 +1345,25 @@ func checkReferenceGrant(ctx context.Context, cli client.Client, obj v1beta1.Ref
return false
}

// CheckConsumerSecretRef reports whether a Consumer in fromNamespace may reference
// the Secret at secretNN, honoring ReferenceGrant for cross-namespace references.
func CheckConsumerSecretRef(ctx context.Context, cli client.Client, fromNamespace string, secretNN k8stypes.NamespacedName) bool {
secretNS := secretNN.Namespace
return checkReferenceGrant(ctx, cli,
v1beta1.ReferenceGrantFrom{
Group: v1beta1.Group(v1alpha1.GroupVersion.Group),
Kind: types.KindConsumer,
Namespace: v1beta1.Namespace(fromNamespace),
},
gatewayv1.ObjectReference{
Group: corev1.GroupName,
Kind: types.KindSecret,
Name: gatewayv1.ObjectName(secretNN.Name),
Namespace: (*gatewayv1.Namespace)(&secretNS),
},
)
Comment on lines +1350 to +1364

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Preserve ReferenceGrant lookup failures separately from denial.

checkReferenceGrant maps cli.List errors to false, so this helper makes the webhook report a missing ReferenceGrant when authorization could not be evaluated. Keep the fail-closed/no-Secret-probe behavior, but propagate or log the lookup error and emit a neutral validation warning instead.

As per coding guidelines, errors must be properly handled and not silently swallowed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/controller/utils.go` around lines 1350 - 1364, Update
CheckConsumerSecretRef and the underlying checkReferenceGrant flow so
ReferenceGrant list failures remain distinguishable from an explicit
authorization denial. Preserve fail-closed behavior and avoid probing the
Secret, but propagate or log the lookup error and have the webhook emit a
neutral validation warning when evaluation fails instead of reporting a missing
ReferenceGrant; ensure the error is not silently swallowed.

Source: Coding guidelines

}

func ListRequests(
ctx context.Context,
c client.Client,
Expand Down
8 changes: 8 additions & 0 deletions internal/webhook/v1/consumer_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,14 @@ func (v *ConsumerCustomValidator) collectWarnings(ctx context.Context, consumer
}
visited[nn] = struct{}{}

// Don't probe cross-namespace Secrets that no ReferenceGrant permits: the
// found/not-found warning difference would leak Secret existence across
// namespaces. Emit a uniform message and skip the lookup.
if namespace != defaultNamespace && !controller.CheckConsumerSecretRef(ctx, v.Client, defaultNamespace, nn) {
warnings = append(warnings, fmt.Sprintf("Referenced Secret '%s/%s' is not accessible from this Consumer without a ReferenceGrant", nn.Namespace, nn.Name))
continue
}

warnings = append(warnings, v.checker.Secret(ctx, reference.SecretRef{
Object: consumer,
NamespacedName: nn,
Expand Down
44 changes: 44 additions & 0 deletions internal/webhook/v1/consumer_webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,50 @@ func TestConsumerValidator_CrossNamespaceSecretRefDeniedWithoutGrant(t *testing.
require.Contains(t, err.Error(), "not permitted by any ReferenceGrant")
}

// The rejection above must not double as an existence oracle: a cross-namespace ref
// that no ReferenceGrant permits produces the same response whether or not the
// Secret exists.
func TestConsumerValidator_CrossNamespaceSecretOracleSuppressed(t *testing.T) {
enableReferenceGrant(t)
ns := authNS
newConsumer := func() *apisixv1alpha1.Consumer {
return &apisixv1alpha1.Consumer{
ObjectMeta: metav1.ObjectMeta{
Name: "demo",
Namespace: "default",
},
Spec: apisixv1alpha1.ConsumerSpec{
GatewayRef: apisixv1alpha1.GatewayRef{Name: "test-gateway"},
Credentials: []apisixv1alpha1.Credential{{
Type: "jwt-auth",
SecretRef: &apisixv1alpha1.SecretReference{
Name: "jwt-secret",
Namespace: &ns,
},
}},
},
}
}

// Secret present in the foreign namespace, no ReferenceGrant permitting the ref.
present := buildConsumerValidator(t, &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "jwt-secret", Namespace: authNS},
})
presentWarnings, presentErr := present.ValidateCreate(context.Background(), newConsumer())
require.Error(t, presentErr)

// Same request, Secret absent.
absent := buildConsumerValidator(t)
absentWarnings, absentErr := absent.ValidateCreate(context.Background(), newConsumer())
require.Error(t, absentErr)

// Identical response either way: no existence oracle.
require.Equal(t, absentWarnings, presentWarnings)
require.Equal(t, absentErr.Error(), presentErr.Error())
require.Len(t, presentWarnings, 1)
require.Contains(t, presentWarnings[0], "Referenced Secret 'auth/jwt-secret' is not accessible from this Consumer without a ReferenceGrant")
}

func TestConsumerValidator_NoWarnings(t *testing.T) {
ns := authNS
consumer := &apisixv1alpha1.Consumer{
Expand Down
Loading