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
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,22 @@ The operator runs the following controllers and webhooks:
| **NetworkPolicy Controller** | Creates permissive or restrictive NetworkPolicies based on signature verification status |
| **MLflow Controller** | Auto-discovers MLflow instances, creates experiments per agent, injects tracking env vars and RBAC |

## Bundle Service

Kagenti includes a dedicated bundle service used by AuthBridge clients to fetch authorization bundles.

This service is deployed using the manifests in `kagenti-operator/config/bundleservice/` and is intended for SRE operational use.

Key facts:

- Deployment name: `bundle-service`
- Namespace: `system`
- Service type: `ClusterIP`
- Port: `8080`
- Health endpoints: `/healthz`, `/readyz`

Use `kagenti-operator/kagenti-operator/cmd/bundle-service/README.md` for SRE runbook guidance and operational details.

## Quick Start

### Prerequisites
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: authorizationpolicies.agent.kagenti.dev
spec:
group: agent.kagenti.dev
names:
kind: AuthorizationPolicy
listKind: AuthorizationPolicyList
plural: authorizationpolicies
singular: authorizationpolicy
shortNames:
- ap
scope: Namespaced
versions:
- name: v1alpha1
served: true
storage: true
subresources:
status: {}
additionalPrinterColumns:
- name: Scope
type: string
jsonPath: .spec.scope
- name: ClientID
type: string
jsonPath: .spec.clientID
- name: Hash
type: string
jsonPath: .status.bundleHash
priority: 1
- name: Age
type: date
jsonPath: .metadata.creationTimestamp
schema:
openAPIV3Schema:
type: object
required:
- spec
properties:
spec:
type: object
required:
- scope
- policies
properties:
scope:
type: string
enum:
- global
- namespace
- client
default: client
clientID:
type: string
maxLength: 253
pattern: "^[a-z0-9]([a-z0-9._-]*[a-z0-9])?$"
policies:
type: array
minItems: 1
items:
type: object
required:
- path
- content
properties:
path:
type: string
minLength: 1
pattern: "^[a-z0-9][a-z0-9/_.-]*\\.rego$"
content:
type: string
minLength: 1
x-kubernetes-validations:
- rule: "self.scope == 'client' ? self.clientID != '' : true"
message: "clientID is required when scope is 'client'"
- rule: "self.scope != 'client' ? !has(self.clientID) || self.clientID == '' : true"
message: "clientID must not be set when scope is 'global' or 'namespace'"
status:
type: object
properties:
bundleHash:
type: string
lastBuilt:
type: string
format: date-time
conditions:
type: array
items:
type: object
required:
- type
- status
properties:
type:
type: string
status:
type: string
enum:
- "True"
- "False"
- "Unknown"
lastTransitionTime:
type: string
format: date-time
reason:
type: string
message:
type: string
40 changes: 40 additions & 0 deletions kagenti-operator/api/v1alpha1/authorizationpolicy_conversion.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
Copyright 2025.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package v1alpha1

import (
"fmt"

"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
)

func AuthorizationPolicyFromUnstructured(obj *unstructured.Unstructured) (*AuthorizationPolicy, error) {
var ap AuthorizationPolicy
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &ap); err != nil {
return nil, fmt.Errorf("converting from unstructured: %w", err)
}
return &ap, nil
}

func AuthorizationPolicyToUnstructured(ap *AuthorizationPolicy) (*unstructured.Unstructured, error) {
obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(ap)
if err != nil {
return nil, fmt.Errorf("converting to unstructured: %w", err)
}
return &unstructured.Unstructured{Object: obj}, nil
}
141 changes: 141 additions & 0 deletions kagenti-operator/api/v1alpha1/authorizationpolicy_conversion_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/*
Copyright 2025.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package v1alpha1

import (
"testing"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)

func TestAuthorizationPolicyFromUnstructured(t *testing.T) {
obj := &unstructured.Unstructured{
Object: map[string]any{
"apiVersion": "agent.kagenti.dev/v1alpha1",
"kind": "AuthorizationPolicy",
"metadata": map[string]any{
"name": "test-client",
"namespace": "default",
},
"spec": map[string]any{
"scope": "client",
"clientID": "test-client",
"policies": []any{
map[string]any{
"path": "inbound/request.rego",
"content": "package authbridge.client\ndefault allow := true\n",
},
},
},
},
}

ap, err := AuthorizationPolicyFromUnstructured(obj)
if err != nil {
t.Fatalf("AuthorizationPolicyFromUnstructured failed: %v", err)
}
if ap.Spec.Scope != PolicyScopeClient {
t.Fatalf("unexpected scope: %s", ap.Spec.Scope)
}
if ap.Spec.ClientID != "test-client" {
t.Fatalf("unexpected clientID: %s", ap.Spec.ClientID)
}
if len(ap.Spec.Policies) != 1 {
t.Fatalf("expected 1 policy, got %d", len(ap.Spec.Policies))
}
}

func TestAuthorizationPolicyFromUnstructured_Global(t *testing.T) {
obj := &unstructured.Unstructured{
Object: map[string]any{
"apiVersion": "agent.kagenti.dev/v1alpha1",
"kind": "AuthorizationPolicy",
"metadata": map[string]any{
"name": "global-policy",
"namespace": "kagenti-system",
},
"spec": map[string]any{
"scope": "global",
"policies": []any{
map[string]any{
"path": "inbound/request.rego",
"content": "package authbridge.global\ndefault allow := true\n",
},
},
},
},
}

ap, err := AuthorizationPolicyFromUnstructured(obj)
if err != nil {
t.Fatalf("AuthorizationPolicyFromUnstructured failed: %v", err)
}
if ap.Spec.Scope != PolicyScopeGlobal {
t.Fatalf("unexpected scope: %s", ap.Spec.Scope)
}
if ap.Spec.ClientID != "" {
t.Fatalf("expected empty clientID for global, got: %s", ap.Spec.ClientID)
}
}

func TestAuthorizationPolicyRoundTrip(t *testing.T) {
original := &AuthorizationPolicy{
TypeMeta: metav1.TypeMeta{
APIVersion: "agent.kagenti.dev/v1alpha1",
Kind: "AuthorizationPolicy",
},
ObjectMeta: metav1.ObjectMeta{
Name: "my-client",
Namespace: "default",
},
Spec: AuthorizationPolicySpec{
Scope: PolicyScopeClient,
ClientID: "my-client",
Policies: []PolicyEntry{
{
Path: "inbound/request.rego",
Content: "package authbridge.client\ndefault allow := false\n",
},
{
Path: "outbound/request.rego",
Content: "package authbridge.client\ndefault allow := true\n",
},
},
},
}

obj, err := AuthorizationPolicyToUnstructured(original)
if err != nil {
t.Fatalf("AuthorizationPolicyToUnstructured failed: %v", err)
}

roundTripped, err := AuthorizationPolicyFromUnstructured(obj)
if err != nil {
t.Fatalf("AuthorizationPolicyFromUnstructured failed: %v", err)
}

if roundTripped.Spec.Scope != original.Spec.Scope {
t.Fatalf("scope mismatch: %s vs %s", roundTripped.Spec.Scope, original.Spec.Scope)
}
if roundTripped.Spec.ClientID != original.Spec.ClientID {
t.Fatalf("clientID mismatch: %s vs %s", roundTripped.Spec.ClientID, original.Spec.ClientID)
}
if len(roundTripped.Spec.Policies) != len(original.Spec.Policies) {
t.Fatalf("policy count mismatch: %d vs %d", len(roundTripped.Spec.Policies), len(original.Spec.Policies))
}
}
Loading
Loading