Newer
Older
EnvoyControlPlane / internal / pkg / snapshot / rbac.go
package snapshot

import (
	"context"
	"fmt"

	corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
	listenerv3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
	rbacv3 "github.com/envoyproxy/go-control-plane/envoy/config/rbac/v3"
	network_rbacv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/rbac/v3"
	cachev3 "github.com/envoyproxy/go-control-plane/pkg/cache/v3"
	"github.com/envoyproxy/go-control-plane/pkg/cache/types"
	resourcev3 "github.com/envoyproxy/go-control-plane/pkg/resource/v3"
	"google.golang.org/protobuf/types/known/anypb"
	"google.golang.org/protobuf/types/known/wrapperspb"
)

const rbacFilterName = "envoy.filters.network.rbac"

// buildRBACFilter constructs a network RBAC filter that DENYs the given IPs.
// Returns nil when blockedIPs is empty (caller should strip any existing filter).
func buildRBACFilter(blockedIPs []string) (*listenerv3.Filter, error) {
	if len(blockedIPs) == 0 {
		return nil, nil
	}

	principals := make([]*rbacv3.Principal, 0, len(blockedIPs))
	for _, ip := range blockedIPs {
		principals = append(principals, &rbacv3.Principal{
			Identifier: &rbacv3.Principal_RemoteIp{
				RemoteIp: &corev3.CidrRange{
					AddressPrefix: ip,
					PrefixLen:     wrapperspb.UInt32(32),
				},
			},
		})
	}

	rbacConfig := &network_rbacv3.RBAC{
		StatPrefix: "netwatch_block",
		Rules: &rbacv3.RBAC{
			Action: rbacv3.RBAC_DENY,
			Policies: map[string]*rbacv3.Policy{
				"blocked-ips": {
					Permissions: []*rbacv3.Permission{
						{Rule: &rbacv3.Permission_Any{Any: true}},
					},
					Principals: principals,
				},
			},
		},
	}

	typedConfig, err := anypb.New(rbacConfig)
	if err != nil {
		return nil, fmt.Errorf("failed to marshal RBAC config: %w", err)
	}

	return &listenerv3.Filter{
		Name:       rbacFilterName,
		ConfigType: &listenerv3.Filter_TypedConfig{TypedConfig: typedConfig},
	}, nil
}

// InjectRBACIntoAllListeners prepends a DENY RBAC filter to every filter chain
// of every listener. If blockedIPs is empty, any existing RBAC filter is removed.
// The snapshot version is bumped so Envoy picks up the change immediately.
func (sm *SnapshotManager) InjectRBACIntoAllListeners(ctx context.Context, blockedIPs []string) error {
	snap, err := sm.Cache.GetSnapshot(sm.NodeID)
	if err != nil {
		return fmt.Errorf("no active snapshot: %w", err)
	}

	rbacFilter, err := buildRBACFilter(blockedIPs)
	if err != nil {
		return err
	}

	// Copy all non-listener resources unchanged
	resources := map[resourcev3.Type][]types.Resource{}
	for _, rtype := range []resourcev3.Type{
		resourcev3.ClusterType,
		resourcev3.RouteType,
		resourcev3.SecretType,
		resourcev3.ExtensionConfigType,
	} {
		for _, v := range snap.GetResources(rtype) {
			resources[rtype] = append(resources[rtype], v)
		}
	}

	// Mutate listeners in-place (they're already cloned from DB on each update)
	for _, item := range snap.GetResources(resourcev3.ListenerType) {
		listener, ok := item.(*listenerv3.Listener)
		if !ok {
			resources[resourcev3.ListenerType] = append(resources[resourcev3.ListenerType], item)
			continue
		}
		for _, fc := range listener.GetFilterChains() {
			// Strip any previous RBAC filter
			stripped := make([]*listenerv3.Filter, 0, len(fc.Filters))
			for _, f := range fc.Filters {
				if f.GetName() != rbacFilterName {
					stripped = append(stripped, f)
				}
			}
			if rbacFilter != nil {
				fc.Filters = append([]*listenerv3.Filter{rbacFilter}, stripped...)
			} else {
				fc.Filters = stripped
			}
		}
		resources[resourcev3.ListenerType] = append(resources[resourcev3.ListenerType], listener)
	}

	newSnap, err := cachev3.NewSnapshot(
		fmt.Sprintf("rbac-%d", len(blockedIPs)),
		resources,
	)
	if err != nil {
		return fmt.Errorf("failed to create RBAC snapshot: %w", err)
	}
	return sm.Cache.SetSnapshot(ctx, sm.NodeID, newSnap)
}