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
3 changes: 3 additions & 0 deletions apps/api/cmd/api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,9 @@ func startAPI(cfg *config.Config) {
storageService := storage_service.NewStorageService(cfg, repo, kubeClient, promClient, serviceService)
go storageService.StoreDatabaseVolumeNames(ctx)
emailService := email_service.NewEmailService(cfg, repo, kubeClient, mail.SMTPSender{})
if err := k8s.EnsurePrivilegedNamespace(ctx, kubeClient.GetInternalClient(), cfg.GetSystemNamespace()); err != nil {
log.Errorf("Failed to set the Pod Security level of the system namespace: %v", err)
}
networkPolicyManager := k8s.NewNetworkPolicyManager(repo, kubeClient.GetInternalClient())
if err := networkPolicyManager.Sync(ctx); err != nil {
log.Errorf("Failed to sync network policies: %v", err)
Expand Down
30 changes: 30 additions & 0 deletions apps/api/internal/infrastructure/k8s/pod_security.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package k8s

import (
"context"
"fmt"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/kubernetes"
)

const PodSecurityEnforceLabel = "pod-security.kubernetes.io/enforce"

// EnsurePrivilegedNamespace lets the system namespace run buildkit, alloy and node-exporter
// on clusters that enforce a stricter Pod Security level by default. A level already set is kept.
func EnsurePrivilegedNamespace(ctx context.Context, client kubernetes.Interface, name string) error {
namespace, err := client.CoreV1().Namespaces().Get(ctx, name, metav1.GetOptions{})
if err != nil {
return fmt.Errorf("failed to get namespace %s: %w", name, err)
}
if _, ok := namespace.Labels[PodSecurityEnforceLabel]; ok {
return nil
}

patch := fmt.Appendf(nil, `{"metadata":{"labels":{%q:"privileged"}}}`, PodSecurityEnforceLabel)
if _, err := client.CoreV1().Namespaces().Patch(ctx, name, types.MergePatchType, patch, metav1.PatchOptions{}); err != nil {
return fmt.Errorf("failed to label namespace %s: %w", name, err)
}
return nil
}
51 changes: 51 additions & 0 deletions apps/api/internal/infrastructure/k8s/pod_security_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package k8s

import (
"context"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/fake"
)

func TestEnsurePrivilegedNamespace(t *testing.T) {
tests := []struct {
name string
labels map[string]string
want map[string]string
}{
{
name: "unlabeled gets privileged",
labels: map[string]string{"kubernetes.io/metadata.name": "unbind-system"},
want: map[string]string{"kubernetes.io/metadata.name": "unbind-system", PodSecurityEnforceLabel: "privileged"},
},
{
name: "a level set by hand is kept",
labels: map[string]string{PodSecurityEnforceLabel: "baseline"},
want: map[string]string{PodSecurityEnforceLabel: "baseline"},
},
{
name: "no labels at all",
labels: nil,
want: map[string]string{PodSecurityEnforceLabel: "privileged"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := fake.NewClientset(&corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "unbind-system", Labels: tt.labels}})

require.NoError(t, EnsurePrivilegedNamespace(context.Background(), client, "unbind-system"))

namespace, err := client.CoreV1().Namespaces().Get(context.Background(), "unbind-system", metav1.GetOptions{})
require.NoError(t, err)
assert.Equal(t, tt.want, namespace.Labels)
})
}
}

func TestEnsurePrivilegedNamespaceMissing(t *testing.T) {
assert.Error(t, EnsurePrivilegedNamespace(context.Background(), fake.NewClientset(), "unbind-system"))
}
1 change: 1 addition & 0 deletions apps/docs/content/operations/bring-your-own-kubernetes.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ Use the latest version from the [releases page](https://github.com/unbindapp/unb
- **Redis**: the bundled Redis takes a generated password from the `redis` secret, and a NetworkPolicy limits it to the API. Your cluster's network plugin has to enforce NetworkPolicy for that limit to hold. Bringing your own Redis means setting `overrides.unbind.api.redis.url` and the password secret, see the chart README.
- **Network isolation**: Unbind keeps teams apart with NetworkPolicies, which also need a network plugin that enforces them. See [Network Isolation](/operations/network-isolation).
- **Build workers** run one per node, so their replica count cannot exceed the number of nodes.
- **Pod Security**: the build workers and the metrics and logging agents need privileged pods, so the install labels `unbind-system` `pod-security.kubernetes.io/enforce: privileged` unless it already has a level. Team namespaces keep the cluster's default. Where that default is `baseline`, as on Talos, templates that need extra capabilities, like WireGuard and Gluetun, cannot start.
- **Bringing your own logging or metrics**: you can turn components off, but then Unbind cannot configure them from the UI, and they have to be set up the way Unbind expects. Logs need the `unbind-team`, `unbind-project`, `unbind-environment`, `unbind-service` and `unbind-deployment` labels indexed. Metrics need kube-state-metrics started with `--metric-labels-allowlist=pods=[*]`.

## What you do not get
Expand Down
2 changes: 2 additions & 0 deletions deploy/charts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ By default, and by the installer (`apps/installer`), all resources are tied to t

This gives the unbind service account access to all of its resources (as it has superuser permissions to its own namespace)

Buildkit, alloy and node-exporter need privileged pods, so the presync hook on their releases labels the namespace `pod-security.kubernetes.io/enforce: privileged` when it has no level yet. The API does the same at startup, which is how existing installs get it.

## Bringing your own cluster

**Wild card domain**
Expand Down
19 changes: 19 additions & 0 deletions deploy/charts/helmfile.yaml.gotmpl
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,21 @@ releases:
namespace: {{ .Values.namespace }}
chart: vm/victoria-metrics-k8s-stack
version: 0.95.2
# Hardened distros like Talos enforce baseline by default; a level already set is kept
hooks:
- &privilegedNamespace
events: ["presync"]
showlogs: true
command: "/bin/sh"
args:
- "-c"
- |
set -eu
NAMESPACE="{{ .Values.namespace }}"
kubectl create namespace "$NAMESPACE" 2>/dev/null || true
if [ -z "$(kubectl get namespace "$NAMESPACE" -o jsonpath='{.metadata.labels.pod-security\.kubernetes\.io/enforce}')" ]; then
kubectl label --overwrite namespace "$NAMESPACE" pod-security.kubernetes.io/enforce=privileged
fi
values:
- values/victoria-metrics-k8s-stack.yaml
- vmsingle:
Expand All @@ -134,6 +149,8 @@ releases:
namespace: {{ .Values.namespace }}
chart: grafana/alloy
version: 1.13.1
hooks:
- *privilegedNamespace
values:
-
alloy:
Expand Down Expand Up @@ -337,6 +354,8 @@ releases:
- name: buildkit
namespace: {{ .Values.namespace }}
chart: ./charts/buildkitd
hooks:
- *privilegedNamespace
values:
-
resources: {}
Expand Down