feat(ory): OIDC group-to-team mapping, social login, Gitea OIDC-only mode

Identity permissions flow from Kratos metadata_admin.groups through
Hydra ID token claims to Gitea's OIDC group-to-team mapping:
- super-admin → site admin + Owners + Employees teams
- employee → Owners + Employees teams
- community → Contributors team (social sign-up users)

Kratos: Discord + GitHub social login providers, community identity
schema, OIDC method enabled with env-var credential injection via VSO.

Gitea: OIDC-only login (no local registration, no password form),
APP_NAME, favicon, auto-registration with account linking.

Also: messages-mta-in recreate strategy + liveness probe for milter.
This commit is contained in:
2026-03-27 17:46:11 +00:00
parent 33f0e44545
commit 97628b0f6f
5 changed files with 136 additions and 0 deletions

File diff suppressed because one or more lines are too long

View File

@@ -19,6 +19,9 @@ gitea:
email: gitea@local.domain
config:
DEFAULT:
APP_NAME: "Sunbeam Studios Version Control"
ui:
DEFAULT_THEME: sunbeam
THEMES: sunbeam
@@ -56,11 +59,24 @@ gitea:
TYPE: redis
CONN_STR: redis://valkey.data.svc.cluster.local:6379/2?pool_size=100&idle_timeout=180s
service:
# Only allow registration through OIDC (Hydra/Kratos), not local accounts.
DISABLE_REGISTRATION: "false"
ALLOW_ONLY_EXTERNAL_REGISTRATION: "true"
# Hide the password login form — show only the OIDC button.
ENABLE_PASSWORD_SIGNIN_FORM: "false"
openid:
ENABLE_OPENID_SIGNIN: "false"
ENABLE_OPENID_SIGNUP: "false"
oauth2_client:
# Auto-redirect to Hydra OIDC on login — makes OIDC the primary auth method.
OAUTH2_AUTO_REDIRECT_TO_PROVIDER: Sunbeam
# Register new OIDC users automatically.
ENABLE_AUTO_REGISTRATION: "true"
ACCOUNT_LINKING: auto
USERNAME: preferred_username
storage:
STORAGE_TYPE: minio
@@ -112,6 +128,10 @@ extraContainerVolumeMounts:
mountPath: /data/gitea/public/assets/img/logo.svg
subPath: logo.svg
readOnly: true
- name: custom-theme
mountPath: /data/gitea/public/assets/img/favicon.png
subPath: favicon.png
readOnly: true
- name: mkcert-ca
mountPath: /run/ca/ca.crt
subPath: ca.crt

View File

@@ -5,6 +5,10 @@ metadata:
namespace: lasuite
spec:
replicas: 1
# Recreate: hostPort 25 blocks RollingUpdate — the new pod can't
# schedule while the old one still holds the port.
strategy:
type: Recreate
selector:
matchLabels:
app: messages-mta-in
@@ -31,6 +35,26 @@ spec:
key: MDA_API_SECRET
- name: MAX_INCOMING_EMAIL_SIZE
value: "30000000"
# Liveness: verify the delivery milter process is running and the
# unix socket exists. The milter is a long-lived Python process that
# can hang silently after days of uptime (COE-2026-002 addendum).
# Without this probe, postfix returns 451 to all inbound mail and
# nobody notices until senders complain.
livenessProbe:
exec:
command:
- sh
- -c
- "test -S /var/spool/postfix/milter/delivery.sock && kill -0 $(cat /var/run/milter.pid 2>/dev/null || pgrep -f delivery_milter.py)"
initialDelaySeconds: 15
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
tcpSocket:
port: 25
initialDelaySeconds: 10
periodSeconds: 15
securityContext:
capabilities:
add: ["NET_BIND_SERVICE"]

View File

@@ -46,6 +46,25 @@ kratos:
- https://auth.DOMAIN_SUFFIX
lookup_secret:
enabled: true
oidc:
enabled: true
config:
providers:
- id: discord
provider: discord
client_id: $DISCORD_CLIENT_ID
client_secret: $DISCORD_CLIENT_SECRET
scope:
- identify
- email
mapper_url: "base64://eyJpZCI6ICJ7eyBpZiAucHJvdmlkZXJfaWQgfX17eyAucHJvdmlkZXJfaWQgfX17eyBlbHNlIH19e3sgLnByb3ZpZGVyIH19e3sgZW5kIH19Ont7IC5zdWIgfX0iLCAidHJhaXRzIjogeyJlbWFpbCI6ICJ7eyAuZW1haWwgfX0iLCAibmlja25hbWUiOiAie3sgLnVzZXJuYW1lIH19IiwgInBpY3R1cmUiOiAie3sgaWYgLmF2YXRhciB9fWh0dHBzOi8vY2RuLmRpc2NvcmRhcHAuY29tL2F2YXRhcnMve3sgLnN1YiB9fS97eyAuYXZhdGFyIH19LnBuZ3t7IGVuZCB9fSJ9LCAibWV0YWRhdGFfcHVibGljIjogeyJwcm92aWRlciI6ICJkaXNjb3JkIn19"
- id: github
provider: github
client_id: $GITHUB_CLIENT_ID
client_secret: $GITHUB_CLIENT_SECRET
scope:
- user:email
mapper_url: "base64://eyJpZCI6ICJ7eyBpZiAucHJvdmlkZXJfaWQgfX17eyAucHJvdmlkZXJfaWQgfX17eyBlbHNlIH19e3sgLnByb3ZpZGVyIH19e3sgZW5kIH19Ont7IC5zdWIgfX0iLCAidHJhaXRzIjogeyJlbWFpbCI6ICJ7eyAuZW1haWwgfX0iLCAibmlja25hbWUiOiAie3sgLmxvZ2luIH19IiwgImdpdmVuX25hbWUiOiAie3sgLm5hbWUgfX0iLCAicGljdHVyZSI6ICJ7eyAuYXZhdGFyX3VybCB9fSJ9LCAibWV0YWRhdGFfcHVibGljIjogeyJwcm92aWRlciI6ICJnaXRodWIifX0="
flows:
error:
ui_url: https://auth.DOMAIN_SUFFIX/error
@@ -78,6 +97,8 @@ kratos:
url: base64://ewogICIkaWQiOiAiaHR0cHM6Ly9zY2hlbWFzLnN1bmJlYW0uc3R1ZGlvL2lkZW50aXR5Lmpzb24iLAogICIkc2NoZW1hIjogImh0dHA6Ly9qc29uLXNjaGVtYS5vcmcvZHJhZnQtMDcvc2NoZW1hIyIsCiAgInR5cGUiOiAib2JqZWN0IiwKICAidGl0bGUiOiAiUGVyc29uIChsZWdhY3kpIiwKICAicHJvcGVydGllcyI6IHsKICAgICJ0cmFpdHMiOiB7CiAgICAgICJ0eXBlIjogIm9iamVjdCIsCiAgICAgICJwcm9wZXJ0aWVzIjogewogICAgICAgICJlbWFpbCI6IHsKICAgICAgICAgICJ0eXBlIjogInN0cmluZyIsCiAgICAgICAgICAiZm9ybWF0IjogImVtYWlsIiwKICAgICAgICAgICJ0aXRsZSI6ICJFbWFpbCIsCiAgICAgICAgICAib3J5LnNoL2tyYXRvcyI6IHsKICAgICAgICAgICAgImNyZWRlbnRpYWxzIjogewogICAgICAgICAgICAgICJwYXNzd29yZCI6IHsKICAgICAgICAgICAgICAgICJpZGVudGlmaWVyIjogdHJ1ZQogICAgICAgICAgICAgIH0KICAgICAgICAgICAgfSwKICAgICAgICAgICAgInJlY292ZXJ5IjogewogICAgICAgICAgICAgICJ2aWEiOiAiZW1haWwiCiAgICAgICAgICAgIH0sCiAgICAgICAgICAgICJ2ZXJpZmljYXRpb24iOiB7CiAgICAgICAgICAgICAgInZpYSI6ICJlbWFpbCIKICAgICAgICAgICAgfQogICAgICAgICAgfQogICAgICAgIH0sCiAgICAgICAgIm5hbWUiOiB7CiAgICAgICAgICAidHlwZSI6ICJvYmplY3QiLAogICAgICAgICAgInByb3BlcnRpZXMiOiB7CiAgICAgICAgICAgICJmaXJzdCI6IHsKICAgICAgICAgICAgICAidHlwZSI6ICJzdHJpbmciLAogICAgICAgICAgICAgICJ0aXRsZSI6ICJGaXJzdCBuYW1lIgogICAgICAgICAgICB9LAogICAgICAgICAgICAibGFzdCI6IHsKICAgICAgICAgICAgICAidHlwZSI6ICJzdHJpbmciLAogICAgICAgICAgICAgICJ0aXRsZSI6ICJMYXN0IG5hbWUiCiAgICAgICAgICAgIH0KICAgICAgICAgIH0KICAgICAgICB9CiAgICAgIH0sCiAgICAgICJyZXF1aXJlZCI6IFsKICAgICAgICAiZW1haWwiCiAgICAgIF0sCiAgICAgICJhZGRpdGlvbmFsUHJvcGVydGllcyI6IGZhbHNlCiAgICB9CiAgfQp9Cg==
- id: external
url: base64://ewogICIkaWQiOiAiaHR0cHM6Ly9zY2hlbWFzLnN1bmJlYW0uc3R1ZGlvL2V4dGVybmFsLmpzb24iLAogICIkc2NoZW1hIjogImh0dHA6Ly9qc29uLXNjaGVtYS5vcmcvZHJhZnQtMDcvc2NoZW1hIyIsCiAgInR5cGUiOiAib2JqZWN0IiwKICAidGl0bGUiOiAiRXh0ZXJuYWwgVXNlciIsCiAgInByb3BlcnRpZXMiOiB7CiAgICAidHJhaXRzIjogewogICAgICAidHlwZSI6ICJvYmplY3QiLAogICAgICAicHJvcGVydGllcyI6IHsKICAgICAgICAiZW1haWwiOiB7CiAgICAgICAgICAidHlwZSI6ICJzdHJpbmciLAogICAgICAgICAgImZvcm1hdCI6ICJlbWFpbCIsCiAgICAgICAgICAidGl0bGUiOiAiRW1haWwiLAogICAgICAgICAgIm9yeS5zaC9rcmF0b3MiOiB7CiAgICAgICAgICAgICJjcmVkZW50aWFscyI6IHsKICAgICAgICAgICAgICAicGFzc3dvcmQiOiB7CiAgICAgICAgICAgICAgICAiaWRlbnRpZmllciI6IHRydWUKICAgICAgICAgICAgICB9CiAgICAgICAgICAgIH0sCiAgICAgICAgICAgICJyZWNvdmVyeSI6IHsKICAgICAgICAgICAgICAidmlhIjogImVtYWlsIgogICAgICAgICAgICB9LAogICAgICAgICAgICAidmVyaWZpY2F0aW9uIjogewogICAgICAgICAgICAgICJ2aWEiOiAiZW1haWwiCiAgICAgICAgICAgIH0KICAgICAgICAgIH0KICAgICAgICB9LAogICAgICAgICJnaXZlbl9uYW1lIjogewogICAgICAgICAgInR5cGUiOiAic3RyaW5nIiwKICAgICAgICAgICJ0aXRsZSI6ICJGaXJzdCBuYW1lIgogICAgICAgIH0sCiAgICAgICAgImZhbWlseV9uYW1lIjogewogICAgICAgICAgInR5cGUiOiAic3RyaW5nIiwKICAgICAgICAgICJ0aXRsZSI6ICJMYXN0IG5hbWUiCiAgICAgICAgfSwKICAgICAgICAibmlja25hbWUiOiB7CiAgICAgICAgICAidHlwZSI6ICJzdHJpbmciLAogICAgICAgICAgInRpdGxlIjogIk5pY2tuYW1lIgogICAgICAgIH0sCiAgICAgICAgInBpY3R1cmUiOiB7CiAgICAgICAgICAidHlwZSI6ICJzdHJpbmciLAogICAgICAgICAgImZvcm1hdCI6ICJ1cmkiLAogICAgICAgICAgInRpdGxlIjogIlByb2ZpbGUgcGljdHVyZSIKICAgICAgICB9CiAgICAgIH0sCiAgICAgICJyZXF1aXJlZCI6IFsKICAgICAgICAiZW1haWwiCiAgICAgIF0sCiAgICAgICJhZGRpdGlvbmFsUHJvcGVydGllcyI6IGZhbHNlCiAgICB9CiAgfQp9Cg==
- id: community
url: base64://ewogICIkaWQiOiAiaHR0cHM6Ly9zY2hlbWFzLnN1bmJlYW0uc3R1ZGlvL2NvbW11bml0eS5qc29uIiwKICAiJHNjaGVtYSI6ICJodHRwOi8vanNvbi1zY2hlbWEub3JnL2RyYWZ0LTA3L3NjaGVtYSMiLAogICJ0eXBlIjogIm9iamVjdCIsCiAgInRpdGxlIjogIkNvbW11bml0eSBNZW1iZXIiLAogICJwcm9wZXJ0aWVzIjogewogICAgInRyYWl0cyI6IHsKICAgICAgInR5cGUiOiAib2JqZWN0IiwKICAgICAgInByb3BlcnRpZXMiOiB7CiAgICAgICAgImVtYWlsIjogewogICAgICAgICAgInR5cGUiOiAic3RyaW5nIiwKICAgICAgICAgICJmb3JtYXQiOiAiZW1haWwiLAogICAgICAgICAgInRpdGxlIjogIkVtYWlsIiwKICAgICAgICAgICJvcnkuc2gva3JhdG9zIjogewogICAgICAgICAgICAiY3JlZGVudGlhbHMiOiB7CiAgICAgICAgICAgICAgInBhc3N3b3JkIjogewogICAgICAgICAgICAgICAgImlkZW50aWZpZXIiOiB0cnVlCiAgICAgICAgICAgICAgfSwKICAgICAgICAgICAgICAidG90cCI6IHsKICAgICAgICAgICAgICAgICJhY2NvdW50X25hbWUiOiB0cnVlCiAgICAgICAgICAgICAgfQogICAgICAgICAgICB9LAogICAgICAgICAgICAicmVjb3ZlcnkiOiB7CiAgICAgICAgICAgICAgInZpYSI6ICJlbWFpbCIKICAgICAgICAgICAgfSwKICAgICAgICAgICAgInZlcmlmaWNhdGlvbiI6IHsKICAgICAgICAgICAgICAidmlhIjogImVtYWlsIgogICAgICAgICAgICB9CiAgICAgICAgICB9CiAgICAgICAgfSwKICAgICAgICAiZ2l2ZW5fbmFtZSI6IHsKICAgICAgICAgICJ0eXBlIjogInN0cmluZyIsCiAgICAgICAgICAidGl0bGUiOiAiRmlyc3QgbmFtZSIKICAgICAgICB9LAogICAgICAgICJmYW1pbHlfbmFtZSI6IHsKICAgICAgICAgICJ0eXBlIjogInN0cmluZyIsCiAgICAgICAgICAidGl0bGUiOiAiTGFzdCBuYW1lIgogICAgICAgIH0sCiAgICAgICAgIm5pY2tuYW1lIjogewogICAgICAgICAgInR5cGUiOiAic3RyaW5nIiwKICAgICAgICAgICJ0aXRsZSI6ICJOaWNrbmFtZSIKICAgICAgICB9LAogICAgICAgICJwaWN0dXJlIjogewogICAgICAgICAgInR5cGUiOiAic3RyaW5nIiwKICAgICAgICAgICJmb3JtYXQiOiAidXJpIiwKICAgICAgICAgICJ0aXRsZSI6ICJQcm9maWxlIHBpY3R1cmUiCiAgICAgICAgfQogICAgICB9LAogICAgICAicmVxdWlyZWQiOiBbCiAgICAgICAgImVtYWlsIgogICAgICBdLAogICAgICAiYWRkaXRpb25hbFByb3BlcnRpZXMiOiBmYWxzZQogICAgfQogIH0KfQo=
courier:
smtp:
@@ -133,6 +154,26 @@ deployment:
secretKeyRef:
name: kratos-db-creds
key: dsn
- name: DISCORD_CLIENT_ID
valueFrom:
secretKeyRef:
name: kratos-social-discord
key: client-id
- name: DISCORD_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: kratos-social-discord
key: client-secret
- name: GITHUB_CLIENT_ID
valueFrom:
secretKeyRef:
name: kratos-social-github
key: client-id
- name: GITHUB_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: kratos-social-github
key: client-secret
resources:
limits:
memory: 256Mi

View File

@@ -157,3 +157,51 @@ spec:
text: "{{ index .Secrets \"s3-access-key\" }}"
s3-secret-key:
text: "{{ index .Secrets \"s3-secret-key\" }}"
---
# Discord OAuth2 credentials for Kratos social sign-in.
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: kratos-social-discord
namespace: ory
spec:
vaultAuthRef: vso-auth
mount: secret
type: kv-v2
path: kratos-social-discord
refreshAfter: 30s
destination:
name: kratos-social-discord
create: true
overwrite: true
transformation:
excludeRaw: true
templates:
client-id:
text: "{{ index .Secrets \"client-id\" }}"
client-secret:
text: "{{ index .Secrets \"client-secret\" }}"
---
# GitHub OAuth2 credentials for Kratos social sign-in.
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: kratos-social-github
namespace: ory
spec:
vaultAuthRef: vso-auth
mount: secret
type: kv-v2
path: kratos-social-github
refreshAfter: 30s
destination:
name: kratos-social-github
create: true
overwrite: true
transformation:
excludeRaw: true
templates:
client-id:
text: "{{ index .Secrets \"client-id\" }}"
client-secret:
text: "{{ index .Secrets \"client-secret\" }}"