feat: add homelab-wide Authentik RBAC model and k8s OIDC auth wiring
Adds permissions claim + per-service admin groups in Authentik, scoped Role/RoleBinding per service, public PKCE kubernetes OAuth2 client, and kube-apiserver OIDC extraArgs. Also fixes paperless OIDC signup permissions via adapter override and adds CoreDNS rewrite for authentik.riotpiao.com.
This commit is contained in:
@@ -0,0 +1,95 @@
|
|||||||
|
# Overrides paperless-ngx's own paperless/adapter.py at the same import path
|
||||||
|
# (mounted via subPath in deployment.yaml) - settings.py hardcodes
|
||||||
|
# SOCIALACCOUNT_ADAPTER = "paperless.adapter.CustomSocialAccountAdapter", so
|
||||||
|
# no Django setting needs to change, just the file content underneath it.
|
||||||
|
#
|
||||||
|
# Stock CustomSocialAccountAdapter.populate_user() is a stub ("kept in case
|
||||||
|
# global default permissions are implemented in the future" - they aren't),
|
||||||
|
# so every OIDC signup lands with zero permissions and 403s on every API
|
||||||
|
# endpoint. This adds the actual mapping: Authentik's "permissions" claim
|
||||||
|
# (via the permissions scope, requested in PAPERLESS_SOCIALACCOUNT_PROVIDERS,
|
||||||
|
# computed server-side from group membership by authentik-provision.py) ->
|
||||||
|
# "paperless:write" or "*" (homelab-admins) grants is_staff+is_superuser,
|
||||||
|
# same convention already used for MinIO's policy claim and Grafana's
|
||||||
|
# role_attribute_path. Checking the permission string rather than a literal
|
||||||
|
# group name decouples "what grants access" from which group happens to
|
||||||
|
# hold it - same pattern applies to every other service's Role/RoleBinding
|
||||||
|
# in k8s/infra/rbac/.
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: paperless-adapter
|
||||||
|
data:
|
||||||
|
adapter.py: |
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
|
from allauth.account.adapter import DefaultAccountAdapter
|
||||||
|
from allauth.core import context
|
||||||
|
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
|
||||||
|
from django.conf import settings
|
||||||
|
from django.forms import ValidationError
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
REQUIRED_PERMISSIONS = {"paperless:write", "*"}
|
||||||
|
|
||||||
|
|
||||||
|
class CustomAccountAdapter(DefaultAccountAdapter):
|
||||||
|
def is_open_for_signup(self, request):
|
||||||
|
allow_signups = super().is_open_for_signup(request)
|
||||||
|
return getattr(settings, "ACCOUNT_ALLOW_SIGNUPS", allow_signups)
|
||||||
|
|
||||||
|
def pre_authenticate(self, request, **credentials):
|
||||||
|
if settings.DISABLE_REGULAR_LOGIN:
|
||||||
|
raise ValidationError("Regular login is disabled")
|
||||||
|
return super().pre_authenticate(request, **credentials)
|
||||||
|
|
||||||
|
def is_safe_url(self, url):
|
||||||
|
from django.utils.http import url_has_allowed_host_and_scheme
|
||||||
|
|
||||||
|
allowed_hosts = {context.request.get_host()} | set(settings.ALLOWED_HOSTS)
|
||||||
|
if "*" in allowed_hosts:
|
||||||
|
allowed_hosts.remove("*")
|
||||||
|
allowed_hosts.add(context.request.get_host())
|
||||||
|
return url_has_allowed_host_and_scheme(url, allowed_hosts=allowed_hosts)
|
||||||
|
return url_has_allowed_host_and_scheme(url, allowed_hosts=allowed_hosts)
|
||||||
|
|
||||||
|
def get_reset_password_from_key_url(self, key):
|
||||||
|
if settings.PAPERLESS_URL is None:
|
||||||
|
return super().get_reset_password_from_key_url(key)
|
||||||
|
path = reverse(
|
||||||
|
"account_reset_password_from_key",
|
||||||
|
kwargs={"uidb36": "UID", "key": "KEY"},
|
||||||
|
)
|
||||||
|
path = path.replace("UID-KEY", quote(key))
|
||||||
|
return settings.PAPERLESS_URL + path
|
||||||
|
|
||||||
|
|
||||||
|
class CustomSocialAccountAdapter(DefaultSocialAccountAdapter):
|
||||||
|
def is_open_for_signup(self, request, sociallogin):
|
||||||
|
allow_signups = super().is_open_for_signup(request, sociallogin)
|
||||||
|
return getattr(settings, "SOCIALACCOUNT_ALLOW_SIGNUPS", allow_signups)
|
||||||
|
|
||||||
|
def get_connect_redirect_url(self, request, socialaccount):
|
||||||
|
return reverse("base")
|
||||||
|
|
||||||
|
def populate_user(self, request, sociallogin, data):
|
||||||
|
user = super().populate_user(request, sociallogin, data)
|
||||||
|
perms = set(sociallogin.account.extra_data.get("permissions") or [])
|
||||||
|
if perms & REQUIRED_PERMISSIONS:
|
||||||
|
user.is_staff = True
|
||||||
|
user.is_superuser = True
|
||||||
|
return user
|
||||||
|
|
||||||
|
def save_user(self, request, sociallogin, form=None):
|
||||||
|
# populate_user() sets the flags on the in-memory user, but
|
||||||
|
# allauth's default save_user() re-derives is_staff from
|
||||||
|
# ACCOUNT_DEFAULT_HTTP_PROTOCOL-independent defaults and can
|
||||||
|
# overwrite them on save - re-apply after super().save_user()
|
||||||
|
# persists the row, matching the permissions check above exactly.
|
||||||
|
user = super().save_user(request, sociallogin, form)
|
||||||
|
perms = set(sociallogin.account.extra_data.get("permissions") or [])
|
||||||
|
if perms & REQUIRED_PERMISSIONS and not (user.is_staff and user.is_superuser):
|
||||||
|
user.is_staff = True
|
||||||
|
user.is_superuser = True
|
||||||
|
user.save(update_fields=["is_staff", "is_superuser"])
|
||||||
|
return user
|
||||||
@@ -82,6 +82,13 @@ spec:
|
|||||||
mountPath: /usr/src/paperless/data
|
mountPath: /usr/src/paperless/data
|
||||||
- name: consume
|
- name: consume
|
||||||
mountPath: /usr/src/paperless/consume
|
mountPath: /usr/src/paperless/consume
|
||||||
|
# Overrides paperless-ngx's own adapter.py in place - settings.py
|
||||||
|
# hardcodes the import path, so no Django setting changes, just
|
||||||
|
# the file content underneath it (see adapter-configmap.yaml).
|
||||||
|
- name: adapter
|
||||||
|
mountPath: /usr/src/paperless/src/paperless/adapter.py
|
||||||
|
subPath: adapter.py
|
||||||
|
readOnly: true
|
||||||
volumes:
|
volumes:
|
||||||
- name: media
|
- name: media
|
||||||
persistentVolumeClaim:
|
persistentVolumeClaim:
|
||||||
@@ -91,3 +98,6 @@ spec:
|
|||||||
claimName: paperless-data
|
claimName: paperless-data
|
||||||
- name: consume
|
- name: consume
|
||||||
emptyDir: {}
|
emptyDir: {}
|
||||||
|
- name: adapter
|
||||||
|
configMap:
|
||||||
|
name: paperless-adapter
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ resources:
|
|||||||
- service.yaml
|
- service.yaml
|
||||||
- ingress.yaml
|
- ingress.yaml
|
||||||
- backup-cronjob.yaml
|
- backup-cronjob.yaml
|
||||||
|
- adapter-configmap.yaml
|
||||||
|
- rbac.yaml
|
||||||
# postgres: paperless-db CNPG Cluster, deployed by k8s/infra/databases (wave 2,
|
# postgres: paperless-db CNPG Cluster, deployed by k8s/infra/databases (wave 2,
|
||||||
# before this app at wave 8) - not duplicated here. Same for the paperless-oidc
|
# before this app at wave 8) - not duplicated here. Same for the paperless-oidc
|
||||||
# and paperless-minio-creds Secrets, written by PostSync provisioning Jobs in
|
# and paperless-minio-creds Secrets, written by PostSync provisioning Jobs in
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# Scoped operator access for paperless-admins: restart/config-edit rights on
|
||||||
|
# just this service's own resources, nothing CNPG-managed (paperless-db-*)
|
||||||
|
# or provisioning-managed (paperless-oidc, paperless-minio-creds). Inert
|
||||||
|
# until kube-apiserver's OIDC wiring lands (--oidc-groups-claim=groups,
|
||||||
|
# --oidc-groups-prefix=oidc:) - subject name below assumes that prefix.
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: Role
|
||||||
|
metadata:
|
||||||
|
name: paperless-operator
|
||||||
|
rules:
|
||||||
|
- apiGroups: ["apps"]
|
||||||
|
resources: ["deployments"]
|
||||||
|
resourceNames: ["paperless"]
|
||||||
|
verbs: ["get", "list", "watch", "update", "patch"]
|
||||||
|
- apiGroups: [""]
|
||||||
|
resources: ["configmaps"]
|
||||||
|
resourceNames: ["paperless-config"]
|
||||||
|
verbs: ["get", "list", "watch", "update", "patch"]
|
||||||
|
- apiGroups: [""]
|
||||||
|
resources: ["secrets"]
|
||||||
|
resourceNames: ["paperless-secrets"]
|
||||||
|
verbs: ["get", "list", "watch", "update", "patch"]
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: RoleBinding
|
||||||
|
metadata:
|
||||||
|
name: paperless-admins-binding
|
||||||
|
subjects:
|
||||||
|
- kind: Group
|
||||||
|
name: "oidc:paperless-admins"
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
roleRef:
|
||||||
|
kind: Role
|
||||||
|
name: paperless-operator
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
@@ -203,3 +203,28 @@ spec:
|
|||||||
selfHeal: true
|
selfHeal: true
|
||||||
syncOptions:
|
syncOptions:
|
||||||
- CreateNamespace=true
|
- CreateNamespace=true
|
||||||
|
---
|
||||||
|
# Wave 9 - per-service scoped RBAC (Role/RoleBinding), deliberately last so
|
||||||
|
# every target namespace above already exists. Inert until kube-apiserver
|
||||||
|
# gets --oidc-groups-claim=groups wired up (separate, not-yet-applied
|
||||||
|
# terraform/talosctl change) - these grant nothing until then.
|
||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: Application
|
||||||
|
metadata:
|
||||||
|
name: rbac
|
||||||
|
namespace: argocd
|
||||||
|
annotations:
|
||||||
|
argocd.argoproj.io/sync-wave: "9"
|
||||||
|
spec:
|
||||||
|
project: homelab
|
||||||
|
source:
|
||||||
|
repoURL: https://forgejo.riotpiao.com/rock/homelab.git
|
||||||
|
targetRevision: main
|
||||||
|
path: k8s/infra/rbac
|
||||||
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: default
|
||||||
|
syncPolicy:
|
||||||
|
automated:
|
||||||
|
prune: true
|
||||||
|
selfHeal: true
|
||||||
|
|||||||
@@ -155,6 +155,44 @@ groups_mapping = get_or_create(
|
|||||||
)
|
)
|
||||||
GROUPS_MAPPING_PK = groups_mapping["pk"]
|
GROUPS_MAPPING_PK = groups_mapping["pk"]
|
||||||
|
|
||||||
|
# Generic "permissions" claim, computed from group membership - lets each app
|
||||||
|
# (and eventually k8s RBAC via --oidc-groups-claim) check a permission string
|
||||||
|
# like "paperless:write" instead of hardcoding a group name. homelab-admins
|
||||||
|
# gets "*" (everything); every other admin group gets its own read+write pair.
|
||||||
|
# k8s-devops-admin is declared but has no k8s Role/RoleBinding target yet -
|
||||||
|
# foundation for a future short-lived federated-operator credential.
|
||||||
|
_PERMISSIONS_EXPR = """
|
||||||
|
GROUP_PERMISSIONS = {
|
||||||
|
"homelab-admins": ["*"],
|
||||||
|
"grafana-admins": ["grafana:read", "grafana:write"],
|
||||||
|
"minio-admins": ["minio:read", "minio:write"],
|
||||||
|
"forgejo-admins": ["forgejo:read", "forgejo:write"],
|
||||||
|
"homarr-admins": ["homarr:read", "homarr:write"],
|
||||||
|
"portainer-admins": ["portainer:read", "portainer:write"],
|
||||||
|
"kmsvc-admins": ["kmsvc:read", "kmsvc:write"],
|
||||||
|
"temporal-admins": ["temporal:read", "temporal:write"],
|
||||||
|
"llm-admins": ["llm:read", "llm:write"],
|
||||||
|
"paperless-admins": ["paperless:read", "paperless:write"],
|
||||||
|
"k8s-devops-admin": ["k8s:devops"],
|
||||||
|
}
|
||||||
|
perms = set()
|
||||||
|
for group in request.user.groups.all():
|
||||||
|
perms.update(GROUP_PERMISSIONS.get(group.name, []))
|
||||||
|
return {"permissions": sorted(perms)}
|
||||||
|
""".strip()
|
||||||
|
permissions_mapping = get_or_create(
|
||||||
|
"/api/v3/propertymappings/provider/scope/",
|
||||||
|
"/api/v3/propertymappings/provider/scope/",
|
||||||
|
"scope_name=permissions",
|
||||||
|
{
|
||||||
|
"name": "homelab: permissions claim",
|
||||||
|
"scope_name": "permissions",
|
||||||
|
"expression": _PERMISSIONS_EXPR,
|
||||||
|
},
|
||||||
|
patch_existing={"expression": _PERMISSIONS_EXPR},
|
||||||
|
)
|
||||||
|
PERMISSIONS_MAPPING_PK = permissions_mapping["pk"]
|
||||||
|
|
||||||
# MinIO maps OIDC users to a MinIO policy via a "policy" claim
|
# MinIO maps OIDC users to a MinIO policy via a "policy" claim
|
||||||
# (MINIO_IDENTITY_OPENID_CLAIM_NAME=policy). Emit consoleAdmin (full admin) for
|
# (MINIO_IDENTITY_OPENID_CLAIM_NAME=policy). Emit consoleAdmin (full admin) for
|
||||||
# homelab-admins members, readonly for everyone else. Without this claim MinIO
|
# homelab-admins members, readonly for everyone else. Without this claim MinIO
|
||||||
@@ -180,7 +218,7 @@ POLICY_MAPPING_PK = policy_mapping["pk"]
|
|||||||
# Fetch the standard openid/email/profile mapping pks (shipped by default).
|
# Fetch the standard openid/email/profile mapping pks (shipped by default).
|
||||||
status, res = api("GET", "/api/v3/propertymappings/provider/scope/")
|
status, res = api("GET", "/api/v3/propertymappings/provider/scope/")
|
||||||
by_scope = {m["scope_name"]: m["pk"] for m in res["results"]}
|
by_scope = {m["scope_name"]: m["pk"] for m in res["results"]}
|
||||||
SCOPE_PKS = [by_scope["openid"], by_scope["email"], by_scope["profile"], GROUPS_MAPPING_PK]
|
SCOPE_PKS = [by_scope["openid"], by_scope["email"], by_scope["profile"], GROUPS_MAPPING_PK, PERMISSIONS_MAPPING_PK]
|
||||||
|
|
||||||
status, res = api("GET", "/api/v3/flows/instances/?slug=default-provider-authorization-implicit-consent")
|
status, res = api("GET", "/api/v3/flows/instances/?slug=default-provider-authorization-implicit-consent")
|
||||||
AUTHORIZATION_FLOW_PK = res["results"][0]["pk"]
|
AUTHORIZATION_FLOW_PK = res["results"][0]["pk"]
|
||||||
@@ -190,17 +228,31 @@ status, res = api("GET", "/api/v3/crypto/certificatekeypairs/?has_key=true")
|
|||||||
SIGNING_KEY_PK = res["results"][0]["pk"]
|
SIGNING_KEY_PK = res["results"][0]["pk"]
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
print("[2/5] Ensuring groups homelab-admins / grafana-admins exist...")
|
print("[2/5] Ensuring homelab-admins + per-service admin groups exist...")
|
||||||
homelab_admins = get_or_create(
|
homelab_admins = get_or_create(
|
||||||
"/api/v3/core/groups/", "/api/v3/core/groups/",
|
"/api/v3/core/groups/", "/api/v3/core/groups/",
|
||||||
"name=homelab-admins",
|
"name=homelab-admins",
|
||||||
{"name": "homelab-admins", "is_superuser": True},
|
{"name": "homelab-admins", "is_superuser": True},
|
||||||
)
|
)
|
||||||
grafana_admins = get_or_create(
|
# App-scoped, not Authentik superusers (unlike homelab-admins) - each maps to
|
||||||
"/api/v3/core/groups/", "/api/v3/core/groups/",
|
# read+write in its own service via the "permissions" claim above (k8s Role/
|
||||||
"name=grafana-admins",
|
# RoleBinding in k8s/infra/rbac/, or an app's own adapter e.g. paperless's).
|
||||||
{"name": "grafana-admins", "is_superuser": False},
|
# k8s-devops-admin is declared with no target yet - foundation for a future
|
||||||
)
|
# short-lived federated-operator credential.
|
||||||
|
SERVICE_ADMIN_GROUP_NAMES = [
|
||||||
|
"grafana-admins", "minio-admins", "forgejo-admins", "homarr-admins",
|
||||||
|
"portainer-admins", "kmsvc-admins", "temporal-admins", "llm-admins",
|
||||||
|
"paperless-admins", "k8s-devops-admin",
|
||||||
|
]
|
||||||
|
service_admin_groups = {}
|
||||||
|
for group_name in SERVICE_ADMIN_GROUP_NAMES:
|
||||||
|
service_admin_groups[group_name] = get_or_create(
|
||||||
|
"/api/v3/core/groups/", "/api/v3/core/groups/",
|
||||||
|
f"name={group_name}",
|
||||||
|
{"name": group_name, "is_superuser": False},
|
||||||
|
)
|
||||||
|
grafana_admins = service_admin_groups["grafana-admins"]
|
||||||
|
paperless_admins = service_admin_groups["paperless-admins"]
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
print("[3/5] Ensuring user 'rock' exists with admin group membership...")
|
print("[3/5] Ensuring user 'rock' exists with admin group membership...")
|
||||||
@@ -209,7 +261,7 @@ rock_password = None
|
|||||||
if res.get("results"):
|
if res.get("results"):
|
||||||
rock = res["results"][0]
|
rock = res["results"][0]
|
||||||
status, rock = api("PATCH", f"/api/v3/core/users/{rock['pk']}/", {
|
status, rock = api("PATCH", f"/api/v3/core/users/{rock['pk']}/", {
|
||||||
"groups": [homelab_admins["pk"], grafana_admins["pk"]],
|
"groups": [homelab_admins["pk"]] + [g["pk"] for g in service_admin_groups.values()],
|
||||||
"is_active": True,
|
"is_active": True,
|
||||||
# email is REQUIRED: Grafana's OIDC login reads the email claim from
|
# email is REQUIRED: Grafana's OIDC login reads the email claim from
|
||||||
# userinfo; an empty email makes Grafana fall back to a GitHub-style
|
# userinfo; an empty email makes Grafana fall back to a GitHub-style
|
||||||
@@ -227,7 +279,7 @@ else:
|
|||||||
"is_active": True,
|
"is_active": True,
|
||||||
# Required for Grafana OIDC (see PATCH branch above).
|
# Required for Grafana OIDC (see PATCH branch above).
|
||||||
"email": "[email protected]",
|
"email": "[email protected]",
|
||||||
"groups": [homelab_admins["pk"], grafana_admins["pk"]],
|
"groups": [homelab_admins["pk"]] + [g["pk"] for g in service_admin_groups.values()],
|
||||||
"path": "users",
|
"path": "users",
|
||||||
"type": "internal",
|
"type": "internal",
|
||||||
})
|
})
|
||||||
@@ -342,6 +394,12 @@ for name, cfg in SERVICES.items():
|
|||||||
"secret": client_secret,
|
"secret": client_secret,
|
||||||
"settings": {
|
"settings": {
|
||||||
"server_url": "https://authentik.riotpiao.com/application/o/paperless/.well-known/openid-configuration",
|
"server_url": "https://authentik.riotpiao.com/application/o/paperless/.well-known/openid-configuration",
|
||||||
|
# "groups"/"permissions" aren't default OIDC scopes -
|
||||||
|
# must be requested explicitly for Authentik's scope
|
||||||
|
# mappings above to actually be returned. paperless's
|
||||||
|
# adapter.py ConfigMap reads the "permissions" claim
|
||||||
|
# to grant is_staff+is_superuser.
|
||||||
|
"scope": ["openid", "profile", "email", "groups", "permissions"],
|
||||||
},
|
},
|
||||||
}],
|
}],
|
||||||
},
|
},
|
||||||
@@ -413,6 +471,53 @@ for name, cfg in SERVICES.items():
|
|||||||
app_pks_for_binding.append((name, application["pk"]))
|
app_pks_for_binding.append((name, application["pk"]))
|
||||||
print(f" {name}: provider pk={provider['pk']} application pk={application['pk']}")
|
print(f" {name}: provider pk={provider['pk']} application pk={application['pk']}")
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Separate from the SERVICES loop above: this is a PUBLIC client (PKCE, no
|
||||||
|
# client_secret) for `kubectl` OIDC login, not a confidential-client app
|
||||||
|
# login. Foundation for k8s/infra/rbac/ - kube-apiserver's --oidc-* flags
|
||||||
|
# (controlplane.tftpl) validate tokens issued against this provider.
|
||||||
|
# Redirect URI matches kubelogin's (int128/kubelogin) documented default;
|
||||||
|
# adjust here if a different kubectl OIDC plugin/port is actually used.
|
||||||
|
print("Ensuring public OAuth2 client 'kubernetes' for kubectl OIDC login...")
|
||||||
|
k8s_provider = get_or_create(
|
||||||
|
"/api/v3/providers/oauth2/", "/api/v3/providers/oauth2/",
|
||||||
|
"name=kubernetes",
|
||||||
|
{
|
||||||
|
"name": "kubernetes",
|
||||||
|
"client_id": "kubernetes",
|
||||||
|
"client_type": "public",
|
||||||
|
"authorization_flow": AUTHORIZATION_FLOW_PK,
|
||||||
|
"invalidation_flow": INVALIDATION_FLOW_PK,
|
||||||
|
"signing_key": SIGNING_KEY_PK,
|
||||||
|
"property_mappings": SCOPE_PKS,
|
||||||
|
"sub_mode": "hashed_user_id",
|
||||||
|
"include_claims_in_id_token": True,
|
||||||
|
"grant_types": ["authorization_code", "refresh_token"],
|
||||||
|
"redirect_uris": [
|
||||||
|
{"matching_mode": "strict", "url": "http://localhost:8000"},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
patch_existing={
|
||||||
|
"property_mappings": SCOPE_PKS,
|
||||||
|
"grant_types": ["authorization_code", "refresh_token"],
|
||||||
|
"redirect_uris": [
|
||||||
|
{"matching_mode": "strict", "url": "http://localhost:8000"},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
k8s_application = get_or_create(
|
||||||
|
"/api/v3/core/applications/", "/api/v3/core/applications/",
|
||||||
|
"slug=kubernetes&superuser_full_list=true",
|
||||||
|
{
|
||||||
|
"name": "Kubernetes",
|
||||||
|
"slug": "kubernetes",
|
||||||
|
"provider": k8s_provider["pk"],
|
||||||
|
"meta_launch_url": "https://authentik.riotpiao.com",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
app_pks_for_binding.append(("kubernetes", k8s_application["pk"]))
|
||||||
|
print(f" kubernetes: provider pk={k8s_provider['pk']} application pk={k8s_application['pk']}")
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
print("[5/5] Binding homelab-admins to every application (guaranteed access for rock)...")
|
print("[5/5] Binding homelab-admins to every application (guaranteed access for rock)...")
|
||||||
for name, app_pk in app_pks_for_binding:
|
for name, app_pk in app_pks_for_binding:
|
||||||
@@ -428,9 +533,38 @@ for name, app_pk in app_pks_for_binding:
|
|||||||
)
|
)
|
||||||
print(f" {name}: homelab-admins bound")
|
print(f" {name}: homelab-admins bound")
|
||||||
|
|
||||||
|
# Per-service admin groups are app-scoped (unlike homelab-admins' blanket
|
||||||
|
# binding above) - only grants visibility/access to that one application.
|
||||||
|
# portainer/kmsvc/temporal/llm-serving have no Authentik Application (no OIDC
|
||||||
|
# login integration), so their groups exist for the "permissions" claim /
|
||||||
|
# future k8s RBAC only - nothing to bind here.
|
||||||
|
SERVICE_GROUP_TO_APP_SLUG = {
|
||||||
|
"grafana-admins": "grafana",
|
||||||
|
"minio-admins": "minio",
|
||||||
|
"forgejo-admins": "forgejo",
|
||||||
|
"homarr-admins": "homarr",
|
||||||
|
"paperless-admins": "paperless",
|
||||||
|
}
|
||||||
|
for group_name, app_slug in SERVICE_GROUP_TO_APP_SLUG.items():
|
||||||
|
app_pk = next((pk for n, pk in app_pks_for_binding if n == app_slug), None)
|
||||||
|
if not app_pk:
|
||||||
|
continue
|
||||||
|
group_pk = service_admin_groups[group_name]["pk"]
|
||||||
|
get_or_create(
|
||||||
|
"/api/v3/policies/bindings/", "/api/v3/policies/bindings/",
|
||||||
|
f"target={app_pk}&group={group_pk}",
|
||||||
|
{
|
||||||
|
"target": app_pk,
|
||||||
|
"group": group_pk,
|
||||||
|
"order": 0,
|
||||||
|
"enabled": True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
print(f" {app_slug}: {group_name} bound")
|
||||||
|
|
||||||
print("\nDone. Summary:")
|
print("\nDone. Summary:")
|
||||||
print(" groups: homelab-admins (superuser), grafana-admins")
|
print(" groups: homelab-admins (superuser) + " + ", ".join(SERVICE_ADMIN_GROUP_NAMES))
|
||||||
print(" user: rock -> homelab-admins + grafana-admins")
|
print(" user: rock -> homelab-admins + all service admin groups")
|
||||||
print(f" apps: {', '.join(n for n, _ in app_pks_for_binding)}")
|
print(f" apps: {', '.join(n for n, _ in app_pks_for_binding)}")
|
||||||
if rock_password:
|
if rock_password:
|
||||||
print(" NOTE: rock's password was generated this run - see")
|
print(" NOTE: rock's password was generated this run - see")
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# Scoped operator access for forgejo-admins: the gitea Deployment + admin
|
||||||
|
# creds only. forgejo-db-*, forgejo-oidc, forgejo-tls, and the helm-managed
|
||||||
|
# forgejo-gitea-inline-config stay excluded. Inert until kube-apiserver's
|
||||||
|
# OIDC wiring lands.
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: Role
|
||||||
|
metadata:
|
||||||
|
name: forgejo-operator
|
||||||
|
namespace: cicd
|
||||||
|
rules:
|
||||||
|
- apiGroups: ["apps"]
|
||||||
|
resources: ["deployments"]
|
||||||
|
resourceNames: ["forgejo-gitea"]
|
||||||
|
verbs: ["get", "list", "watch", "update", "patch"]
|
||||||
|
- apiGroups: [""]
|
||||||
|
resources: ["secrets"]
|
||||||
|
resourceNames: ["forgejo-admin"]
|
||||||
|
verbs: ["get", "list", "watch", "update", "patch"]
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: RoleBinding
|
||||||
|
metadata:
|
||||||
|
name: forgejo-admins-binding
|
||||||
|
namespace: cicd
|
||||||
|
subjects:
|
||||||
|
- kind: Group
|
||||||
|
name: "oidc:forgejo-admins"
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
roleRef:
|
||||||
|
kind: Role
|
||||||
|
name: forgejo-operator
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# Scoped operator access for grafana-admins: dashboards + admin creds only.
|
||||||
|
# grafana-oidc (client secret) stays excluded - editing it is a security
|
||||||
|
# change, not app config. Inert until kube-apiserver's OIDC wiring lands.
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: Role
|
||||||
|
metadata:
|
||||||
|
name: grafana-operator
|
||||||
|
namespace: logging
|
||||||
|
rules:
|
||||||
|
- apiGroups: ["apps"]
|
||||||
|
resources: ["deployments"]
|
||||||
|
resourceNames: ["grafana"]
|
||||||
|
verbs: ["get", "list", "watch", "update", "patch"]
|
||||||
|
- apiGroups: [""]
|
||||||
|
resources: ["configmaps"]
|
||||||
|
resourceNames: ["grafana-dashboards-default", "grafana-config-dashboards"]
|
||||||
|
verbs: ["get", "list", "watch", "update", "patch"]
|
||||||
|
- apiGroups: [""]
|
||||||
|
resources: ["secrets"]
|
||||||
|
resourceNames: ["grafana-admin"]
|
||||||
|
verbs: ["get", "list", "watch", "update", "patch"]
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: RoleBinding
|
||||||
|
metadata:
|
||||||
|
name: grafana-admins-binding
|
||||||
|
namespace: logging
|
||||||
|
subjects:
|
||||||
|
- kind: Group
|
||||||
|
name: "oidc:grafana-admins"
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
roleRef:
|
||||||
|
kind: Role
|
||||||
|
name: grafana-operator
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# Scoped operator access for homarr-admins: the homarr Deployment + app
|
||||||
|
# secrets only. homarr-oidc, auth-oidc-secret, db-encryption stay excluded
|
||||||
|
# (security-managed, not app config). Inert until kube-apiserver's OIDC
|
||||||
|
# wiring lands.
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: Role
|
||||||
|
metadata:
|
||||||
|
name: homarr-operator
|
||||||
|
namespace: dashboard
|
||||||
|
rules:
|
||||||
|
- apiGroups: ["apps"]
|
||||||
|
resources: ["deployments"]
|
||||||
|
resourceNames: ["homarr"]
|
||||||
|
verbs: ["get", "list", "watch", "update", "patch"]
|
||||||
|
- apiGroups: [""]
|
||||||
|
resources: ["secrets"]
|
||||||
|
resourceNames: ["homarr-secrets"]
|
||||||
|
verbs: ["get", "list", "watch", "update", "patch"]
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: RoleBinding
|
||||||
|
metadata:
|
||||||
|
name: homarr-admins-binding
|
||||||
|
namespace: dashboard
|
||||||
|
subjects:
|
||||||
|
- kind: Group
|
||||||
|
name: "oidc:homarr-admins"
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
roleRef:
|
||||||
|
kind: Role
|
||||||
|
name: homarr-operator
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# Scoped operator access for kmsvc-admins: management-service + its own
|
||||||
|
# config only. kmsvc-* CA/cluster/pool secrets and the strimzi-cluster-
|
||||||
|
# operator configmap are Strimzi-managed - hand-editing them gets reverted
|
||||||
|
# by the operator's reconcile loop or breaks the Kafka cluster. Inert until
|
||||||
|
# kube-apiserver's OIDC wiring lands.
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: Role
|
||||||
|
metadata:
|
||||||
|
name: kmsvc-operator
|
||||||
|
namespace: sqs
|
||||||
|
rules:
|
||||||
|
- apiGroups: ["apps"]
|
||||||
|
resources: ["deployments"]
|
||||||
|
resourceNames: ["management-service"]
|
||||||
|
verbs: ["get", "list", "watch", "update", "patch"]
|
||||||
|
- apiGroups: [""]
|
||||||
|
resources: ["configmaps"]
|
||||||
|
resourceNames: ["management-service-config"]
|
||||||
|
verbs: ["get", "list", "watch", "update", "patch"]
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: RoleBinding
|
||||||
|
metadata:
|
||||||
|
name: kmsvc-admins-binding
|
||||||
|
namespace: sqs
|
||||||
|
subjects:
|
||||||
|
- kind: Group
|
||||||
|
name: "oidc:kmsvc-admins"
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
roleRef:
|
||||||
|
kind: Role
|
||||||
|
name: kmsvc-operator
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
# NO top-level `namespace:` transformer - each Role/RoleBinding here targets
|
||||||
|
# a different service namespace (logging/storage/cicd/dashboard/sqs/temporal/
|
||||||
|
# llm-serving); a transformer would collapse them all into one, same bug
|
||||||
|
# already fixed once in k8s/infra/minio and k8s/infra/iam. Every resource
|
||||||
|
# here sets its own explicit metadata.namespace.
|
||||||
|
resources:
|
||||||
|
- grafana-operator-role.yaml
|
||||||
|
- minio-operator-role.yaml
|
||||||
|
- forgejo-operator-role.yaml
|
||||||
|
- homarr-operator-role.yaml
|
||||||
|
- portainer-operator-role.yaml
|
||||||
|
- kmsvc-operator-role.yaml
|
||||||
|
- temporal-operator-role.yaml
|
||||||
|
- llm-serving-operator-role.yaml
|
||||||
|
# paperless's Role/RoleBinding lives in k8s/apps/paperless/rbac.yaml instead -
|
||||||
|
# that app already has its own kustomization + namespace, no need to
|
||||||
|
# duplicate it here. All of these stay inert (grant nothing) until
|
||||||
|
# kube-apiserver has --oidc-groups-claim=groups wired up.
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# Scoped operator access for llm-admins: the 4 predictor Deployments only -
|
||||||
|
# no configmap/secret exists in this namespace today. Inert until
|
||||||
|
# kube-apiserver's OIDC wiring lands.
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: Role
|
||||||
|
metadata:
|
||||||
|
name: llm-serving-operator
|
||||||
|
namespace: llm-serving
|
||||||
|
rules:
|
||||||
|
- apiGroups: ["apps"]
|
||||||
|
resources: ["deployments"]
|
||||||
|
resourceNames:
|
||||||
|
- reasoning-predictor
|
||||||
|
- ornith-predictor
|
||||||
|
- embeddings-predictor
|
||||||
|
- reranker-predictor
|
||||||
|
verbs: ["get", "list", "watch", "update", "patch"]
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: RoleBinding
|
||||||
|
metadata:
|
||||||
|
name: llm-admins-binding
|
||||||
|
namespace: llm-serving
|
||||||
|
subjects:
|
||||||
|
- kind: Group
|
||||||
|
name: "oidc:llm-admins"
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
roleRef:
|
||||||
|
kind: Role
|
||||||
|
name: llm-serving-operator
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# Scoped operator access for minio-admins: the tenant StatefulSet + root
|
||||||
|
# creds only - NOT the minio-operator Deployment (shared cluster-wide infra;
|
||||||
|
# editing it risks breaking MinIO for every tenant, not just this one).
|
||||||
|
# minio-oidc/sts-tls stay excluded (security-managed, not app config). Inert
|
||||||
|
# until kube-apiserver's OIDC wiring lands.
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: Role
|
||||||
|
metadata:
|
||||||
|
name: minio-operator-admin
|
||||||
|
namespace: storage
|
||||||
|
rules:
|
||||||
|
- apiGroups: ["apps"]
|
||||||
|
resources: ["statefulsets"]
|
||||||
|
resourceNames: ["minio-cluster-az-a"]
|
||||||
|
verbs: ["get", "list", "watch", "update", "patch"]
|
||||||
|
- apiGroups: [""]
|
||||||
|
resources: ["secrets"]
|
||||||
|
resourceNames: ["minio-creds"]
|
||||||
|
verbs: ["get", "list", "watch", "update", "patch"]
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: RoleBinding
|
||||||
|
metadata:
|
||||||
|
name: minio-admins-binding
|
||||||
|
namespace: storage
|
||||||
|
subjects:
|
||||||
|
- kind: Group
|
||||||
|
name: "oidc:minio-admins"
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
roleRef:
|
||||||
|
kind: Role
|
||||||
|
name: minio-operator-admin
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# Scoped operator access for portainer-admins: just the Deployment - no
|
||||||
|
# configmap/secret exists for portainer today. Inert until kube-apiserver's
|
||||||
|
# OIDC wiring lands.
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: Role
|
||||||
|
metadata:
|
||||||
|
name: portainer-operator
|
||||||
|
namespace: dashboard
|
||||||
|
rules:
|
||||||
|
- apiGroups: ["apps"]
|
||||||
|
resources: ["deployments"]
|
||||||
|
resourceNames: ["portainer"]
|
||||||
|
verbs: ["get", "list", "watch", "update", "patch"]
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: RoleBinding
|
||||||
|
metadata:
|
||||||
|
name: portainer-admins-binding
|
||||||
|
namespace: dashboard
|
||||||
|
subjects:
|
||||||
|
- kind: Group
|
||||||
|
name: "oidc:portainer-admins"
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
roleRef:
|
||||||
|
kind: Role
|
||||||
|
name: portainer-operator
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# Scoped operator access for temporal-admins: temporal-worker + its dynamic
|
||||||
|
# config only. temporal-db-* (CNPG-managed) and webhook-server-cert stay
|
||||||
|
# excluded. Inert until kube-apiserver's OIDC wiring lands.
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: Role
|
||||||
|
metadata:
|
||||||
|
name: temporal-operator
|
||||||
|
namespace: temporal
|
||||||
|
rules:
|
||||||
|
- apiGroups: ["apps"]
|
||||||
|
resources: ["deployments"]
|
||||||
|
resourceNames: ["temporal-worker"]
|
||||||
|
verbs: ["get", "list", "watch", "update", "patch"]
|
||||||
|
- apiGroups: [""]
|
||||||
|
resources: ["configmaps"]
|
||||||
|
resourceNames: ["temporal-dynamic-config"]
|
||||||
|
verbs: ["get", "list", "watch", "update", "patch"]
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: RoleBinding
|
||||||
|
metadata:
|
||||||
|
name: temporal-admins-binding
|
||||||
|
namespace: temporal
|
||||||
|
subjects:
|
||||||
|
- kind: Group
|
||||||
|
name: "oidc:temporal-admins"
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
roleRef:
|
||||||
|
kind: Role
|
||||||
|
name: temporal-operator
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
@@ -33,6 +33,7 @@
|
|||||||
rewrite name homarr.riotpiao.com ingress-nginx-controller.ingress-nginx.svc.cluster.local
|
rewrite name homarr.riotpiao.com ingress-nginx-controller.ingress-nginx.svc.cluster.local
|
||||||
rewrite name portainer.riotpiao.com ingress-nginx-controller.ingress-nginx.svc.cluster.local
|
rewrite name portainer.riotpiao.com ingress-nginx-controller.ingress-nginx.svc.cluster.local
|
||||||
rewrite name longhorn.riotpiao.com ingress-nginx-controller.ingress-nginx.svc.cluster.local
|
rewrite name longhorn.riotpiao.com ingress-nginx-controller.ingress-nginx.svc.cluster.local
|
||||||
|
rewrite name paperless.riotpiao.com ingress-nginx-controller.ingress-nginx.svc.cluster.local
|
||||||
|
|
||||||
kubernetes cluster.local in-addr.arpa ip6.arpa {
|
kubernetes cluster.local in-addr.arpa ip6.arpa {
|
||||||
pods insecure
|
pods insecure
|
||||||
|
|||||||
@@ -33,6 +33,14 @@ machine:
|
|||||||
- ip: ${forgejo_registry_ip}
|
- ip: ${forgejo_registry_ip}
|
||||||
aliases:
|
aliases:
|
||||||
- ${forgejo_hostname}
|
- ${forgejo_hostname}
|
||||||
|
# kube-apiserver's static pod runs on the host network, so it resolves
|
||||||
|
# authentik.riotpiao.com via THIS node's DNS, not cluster CoreDNS - the
|
||||||
|
# OIDC issuer discovery call would otherwise hit the public
|
||||||
|
# Cloudflare-fronted IP instead of nginx directly. Same reasoning as
|
||||||
|
# the forgejo entry above.
|
||||||
|
- ip: 192.168.1.160
|
||||||
|
aliases:
|
||||||
|
- authentik.riotpiao.com
|
||||||
kubelet:
|
kubelet:
|
||||||
image: ${kubelet_image}
|
image: ${kubelet_image}
|
||||||
defaultRuntimeSeccompProfileEnabled: true
|
defaultRuntimeSeccompProfileEnabled: true
|
||||||
@@ -120,6 +128,19 @@ cluster:
|
|||||||
- ${san}
|
- ${san}
|
||||||
%{ endfor ~}
|
%{ endfor ~}
|
||||||
image: ${kube_apiserver_img}
|
image: ${kube_apiserver_img}
|
||||||
|
# Foundation for the Authentik group -> k8s RBAC pattern in
|
||||||
|
# k8s/infra/rbac/ - additive only, existing client-cert auth
|
||||||
|
# (system:masters, this session's own admin@homelab-cluster kubeconfig)
|
||||||
|
# keeps working unchanged; a bad OIDC config just means OIDC logins fail,
|
||||||
|
# not a lockout. Prefixes avoid collision with built-in system:* users
|
||||||
|
# and groups.
|
||||||
|
extraArgs:
|
||||||
|
oidc-issuer-url: https://authentik.riotpiao.com/application/o/kubernetes/
|
||||||
|
oidc-client-id: kubernetes
|
||||||
|
oidc-username-claim: email
|
||||||
|
oidc-groups-claim: groups
|
||||||
|
oidc-username-prefix: "oidc:"
|
||||||
|
oidc-groups-prefix: "oidc:"
|
||||||
admissionControl:
|
admissionControl:
|
||||||
- name: PodSecurity
|
- name: PodSecurity
|
||||||
configuration:
|
configuration:
|
||||||
|
|||||||
Reference in New Issue
Block a user