GITEA__OAUTH2__JWT_SECRET must base64url-decode to 32 bytes. The size-32 claim decoded to 24 bytes, so 1.27 rejected it and generated its own key on each boot; since app.ini lives on an emptyDir that regenerated every restart and logged out all OAuth2 clients. size 43 decodes to 32 bytes, so the rotated key is accepted and stays stable across restarts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
327 lines
11 KiB
YAML
327 lines
11 KiB
YAML
---
|
|
apiVersion: cert-manager.io/v1
|
|
kind: Certificate
|
|
metadata:
|
|
name: gitea
|
|
namespace: gitea
|
|
spec:
|
|
dnsNames:
|
|
- git.k-space.ee
|
|
issuerRef:
|
|
group: cert-manager.io
|
|
kind: ClusterIssuer
|
|
name: default
|
|
secretName: git-tls
|
|
revisionHistoryLimit: 1
|
|
|
|
# Gitea DOES NOT go through Traefik. It has its own IP because ssh :22 would conflict with kube worker ssh. On its own IP, at the moment it doesn't flirt with Traefik — also has its own certificate.
|
|
---
|
|
# After the 2026-09 breach (CVE-2026-59774) all three secrets below leaked
|
|
# because they shared one generated value. INTERNAL_TOKEN and JWT_SECRET were
|
|
# moved to their own claims (gitea-internal-token, gitea-oauth2-jwt) and rotated;
|
|
# the StatefulSet now reads those two from the new claims, so the copies here are
|
|
# unused. SECRET_KEY is intentionally still sourced from this claim and NOT
|
|
# rotated: a new value would make data-at-rest (push-mirror creds, 2FA)
|
|
# undecryptable and break the 2 push mirrors, whose credentials must be re-entered
|
|
# by hand. Do not shrink this mapping: the operator would regenerate the plaintext
|
|
# and change SECRET_KEY. Rotate SECRET_KEY in a maintenance window instead.
|
|
apiVersion: codemowers.cloud/v1beta1
|
|
kind: SecretClaim
|
|
metadata:
|
|
name: gitea-random
|
|
spec:
|
|
size: 32
|
|
mapping:
|
|
- key: GITEA__SECURITY__SECRET_KEY
|
|
value: "%(plaintext)s"
|
|
- key: GITEA__SECURITY__INTERNAL_TOKEN
|
|
value: "%(plaintext)s"
|
|
- key: GITEA__OAUTH2__JWT_SECRET
|
|
value: "%(plaintext)s"
|
|
---
|
|
apiVersion: codemowers.cloud/v1beta1
|
|
kind: SecretClaim
|
|
metadata:
|
|
name: gitea-internal-token
|
|
spec:
|
|
size: 32
|
|
mapping:
|
|
- key: GITEA__SECURITY__INTERNAL_TOKEN
|
|
value: "%(plaintext)s"
|
|
---
|
|
# size 43: GITEA__OAUTH2__JWT_SECRET must base64url-decode to exactly 32 bytes.
|
|
# A 43-char value decodes to 32 bytes; a 32-char one decodes to 24 and Gitea
|
|
# rejects it and generates its own key each boot, which (app.ini is on an
|
|
# emptyDir) changes on every restart and logs out all OAuth2 clients. 43 keeps
|
|
# the rotated key valid and stable.
|
|
apiVersion: codemowers.cloud/v1beta1
|
|
kind: SecretClaim
|
|
metadata:
|
|
name: gitea-oauth2-jwt
|
|
spec:
|
|
size: 43
|
|
mapping:
|
|
- key: GITEA__OAUTH2__JWT_SECRET
|
|
value: "%(plaintext)s"
|
|
---
|
|
apiVersion: codemowers.cloud/v1beta1
|
|
kind: OIDCClient
|
|
metadata:
|
|
name: gitea
|
|
spec:
|
|
displayName: Gitea
|
|
uri: https://git.k-space.ee/user/oauth2/OpenID
|
|
redirectUris:
|
|
- https://git.k-space.ee/user/oauth2/OpenID/callback
|
|
allowedGroups:
|
|
- k-space:floor
|
|
- k-space:friends
|
|
grantTypes:
|
|
- authorization_code
|
|
- refresh_token
|
|
responseTypes:
|
|
- code
|
|
availableScopes:
|
|
- openid
|
|
- profile
|
|
overrideIncomingScopes: true
|
|
pkce: false
|
|
secretRefreshPod:
|
|
apiVersion: v1
|
|
kind: Pod
|
|
metadata:
|
|
name: reset-oidc-config
|
|
spec:
|
|
volumes:
|
|
- name: tmp
|
|
emptyDir: {}
|
|
initContainers:
|
|
- name: jq
|
|
image: mirror.gcr.io/alpine/k8s:1.31.76@sha256:2a3fdd639c71c6cad69fbc8cac2467648855dac29961efec3b155466cc4fa730
|
|
imagePullPolicy: IfNotPresent
|
|
volumeMounts:
|
|
- mountPath: /tmp
|
|
name: tmp
|
|
envFrom:
|
|
- secretRef:
|
|
name: oidc-client-gitea-owner-secrets
|
|
command:
|
|
- /bin/bash
|
|
- -c
|
|
- jq '{"strategyKey":"OpenID","config":{"Provider":"openidConnect","ClientID":$ENV.OIDC_CLIENT_ID,"ClientSecret":$ENV.OIDC_CLIENT_SECRET,"OpenIDConnectAutoDiscoveryURL":"https://auth.k-space.ee/.well-known/openid-configuration","CustomURLMapping":null,"IconURL":"","Scopes":null,"RequiredClaimName":"","RequiredClaimValue":"","GroupClaimName":"","AdminGroup":"","GroupTeamMap":"","GroupTeamMapRemoval":false,"RestrictedGroup":""}} | "UPDATE login_source SET cfg=\(.config|tostring|@sh) WHERE name=\(.strategyKey|tostring|@sh) LIMIT 1"' -n -r > /tmp/update.sql
|
|
containers:
|
|
- name: mysql
|
|
image: mirror.gcr.io/library/mysql:latest
|
|
imagePullPolicy: IfNotPresent
|
|
volumeMounts:
|
|
- mountPath: /tmp
|
|
name: tmp
|
|
env:
|
|
- name: MYSQL_PWD
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: mariadb-secrets
|
|
key: MYSQL_PASSWORD
|
|
command:
|
|
- /bin/bash
|
|
- -c
|
|
- mysql -u gitea gitea -h mariadb -p${MYSQL_PWD} < /tmp/update.sql
|
|
---
|
|
apiVersion: apps/v1
|
|
kind: StatefulSet
|
|
metadata:
|
|
name: gitea
|
|
labels:
|
|
app.kubernetes.io/name: gitea
|
|
spec:
|
|
revisionHistoryLimit: 0
|
|
serviceName: gitea
|
|
replicas: 1
|
|
selector:
|
|
matchLabels:
|
|
app.kubernetes.io/name: gitea
|
|
template:
|
|
metadata:
|
|
labels:
|
|
app.kubernetes.io/name: gitea
|
|
spec:
|
|
enableServiceLinks: false
|
|
securityContext:
|
|
fsGroup: 1000
|
|
runAsUser: 1000
|
|
runAsGroup: 1000
|
|
runAsNonRoot: true
|
|
containers:
|
|
- name: gitea
|
|
# https://github.com/go-gitea/gitea/releases
|
|
image: docker.gitea.com/gitea:1.27.3-rootless
|
|
imagePullPolicy: IfNotPresent
|
|
securityContext:
|
|
readOnlyRootFilesystem: true
|
|
env:
|
|
- name: GITEA__REPOSITORY__DISABLED_REPO_UNITS
|
|
value: repo.releases,repo.wiki
|
|
- name: GITEA__ADMIN__DISABLE_REGULAR_ORG_CREATION
|
|
value: "true"
|
|
- name: GITEA__SERVER__SSH_SERVER_HOST_KEYS
|
|
value: ssh/gitea.rsa,ssh/gitea.ecdsa,ssh/gitea.ed25519
|
|
- name: GITEA__SERVER__START_SSH_SERVER
|
|
value: "true"
|
|
- name: GITEA__SERVER__CERT_FILE
|
|
value: "/cert/tls.crt"
|
|
- name: GITEA__SERVER__KEY_FILE
|
|
value: "/cert/tls.key"
|
|
- name: GITEA__SERVER__SSH_PORT
|
|
value: "22"
|
|
# Port advertised in clone URLs is 22, but the rootless container
|
|
# cannot bind it, so the builtin server listens on 2222 and the
|
|
# Service maps 22 -> 2222. Until 1.26 the image defaulted
|
|
# SSH_LISTEN_PORT to SSH_PORT; it is empty now, which makes Gitea
|
|
# fall back to SSH_PORT and crash with "bind: permission denied".
|
|
- name: GITEA__SERVER__SSH_LISTEN_PORT
|
|
value: "2222"
|
|
- name: GITEA__SERVER__PROTOCOL
|
|
value: https
|
|
- name: GITEA__SERVER__REDIRECT_OTHER_PORT
|
|
value: "true"
|
|
- name: GITEA__SERVER__PORT_TO_REDIRECT
|
|
value: "8080"
|
|
- name: GITEA__SERVER__DOMAIN
|
|
value: git.k-space.ee
|
|
- name: GITEA__SERVER__SSH_DOMAIN
|
|
value: git.k-space.ee
|
|
- name: GITEA__SERVER__HTTP_ADDR
|
|
value: 0.0.0.0
|
|
- name: GITEA__SERVER__ROOT_URL
|
|
value: https://git.k-space.ee
|
|
- name: GITEA__SSH.MINIMUM_KEY_SIZES__DSA
|
|
value: "-1"
|
|
- name: GITEA__DATABASE__DB_TYPE
|
|
value: mysql
|
|
- name: GITEA__DATABASE__HOST
|
|
value: mariadb:3306
|
|
- name: GITEA__DATABASE__NAME
|
|
value: gitea
|
|
- name: GITEA__DATABASE__USER
|
|
value: gitea
|
|
- name: GITEA__DATABASE__SSL_MODE
|
|
value: disable
|
|
- name: GITEA__DATABASE__LOG_SQL
|
|
value: "false"
|
|
- name: GITEA__SECURITY__INSTALL_LOCK
|
|
value: "true"
|
|
# Disable bypassing (disabled) OIDC account. Password-based app tokens remain enabled.
|
|
- name: GITEA__SERVICE__ENABLE_PASSWORD_SIGNIN_FORM
|
|
value: "false"
|
|
- name: GITEA__SERVICE__ENABLE_PASSKEY_AUTHENTICATION
|
|
value: "false"
|
|
- name: GITEA__SERVICE__REGISTER_EMAIL_CONFIRM
|
|
value: "true"
|
|
- name: GITEA__SERVICE__DISABLE_REGISTRATION
|
|
value: "true"
|
|
- name: GITEA__SERVICE__ENABLE_NOTIFY_MAIL
|
|
value: "true"
|
|
- name: GITEA__MAILER__ENABLED
|
|
value: "true"
|
|
- name: GITEA__MAILER__SMTP_ADDR
|
|
value: mail.k-space.ee
|
|
- name: GITEA__MAILER__SMTP_PORT
|
|
value: "465"
|
|
- name: GITEA__MAILER__FROM
|
|
value: Gitea <git@k-space.ee>
|
|
- name: GITEA__MAILER__USER
|
|
value: git
|
|
- name: GITEA__MAILER__USE_PLAIN_TEXT
|
|
value: "false"
|
|
- name: GITEA__SESSION__PROVIDER
|
|
value: file
|
|
- name: GITEA__SESSION__COOKIE_SECURE
|
|
value: "true"
|
|
- name: GITEA__CRON__ENABLED
|
|
value: "true"
|
|
- name: GITEA__OAUTH2_CLIENT__ENABLE_AUTO_REGISTRATION
|
|
value: "true"
|
|
- name: GITEA__DATABASE__PASSWD
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: mariadb-secrets
|
|
key: MYSQL_PASSWORD
|
|
- name: GITEA__MAILER__PASSWD
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: gitea-mail
|
|
key: GITEA__MAILER__PASSWD
|
|
- name: GITEA__OAUTH2__JWT_SECRET
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: gitea-oauth2-jwt
|
|
key: GITEA__OAUTH2__JWT_SECRET
|
|
- name: GITEA__SECURITY__INTERNAL_TOKEN
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: gitea-internal-token
|
|
key: GITEA__SECURITY__INTERNAL_TOKEN
|
|
- name: GITEA__SECURITY__SECRET_KEY
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: gitea-random
|
|
key: GITEA__SECURITY__SECRET_KEY
|
|
ports:
|
|
- containerPort: 8080
|
|
name: http
|
|
- containerPort: 3000
|
|
name: https
|
|
- containerPort: 2222
|
|
name: ssh
|
|
volumeMounts:
|
|
- mountPath: /tmp
|
|
name: tmp
|
|
- mountPath: /etc/gitea
|
|
name: etc
|
|
- mountPath: /cert
|
|
name: cert
|
|
- mountPath: /var/lib/gitea
|
|
name: data
|
|
volumes:
|
|
- name: tmp
|
|
emptyDir: {}
|
|
- name: etc
|
|
emptyDir: {}
|
|
- name: cert
|
|
secret:
|
|
secretName: git-tls
|
|
volumeClaimTemplates:
|
|
- metadata:
|
|
name: data
|
|
spec:
|
|
accessModes:
|
|
- ReadWriteOnce
|
|
storageClassName: gitea
|
|
resources:
|
|
requests:
|
|
storage: 10Gi
|
|
---
|
|
apiVersion: v1
|
|
kind: Service
|
|
metadata:
|
|
name: gitea
|
|
namespace: gitea
|
|
annotations:
|
|
external-dns.alpha.kubernetes.io/hostname: git.k-space.ee
|
|
spec:
|
|
type: LoadBalancer
|
|
externalTrafficPolicy: Local
|
|
selector:
|
|
app.kubernetes.io/name: gitea
|
|
ports:
|
|
- port: 22
|
|
name: ssh
|
|
targetPort: 2222
|
|
- port: 80
|
|
name: http
|
|
targetPort: 8080
|
|
- port: 443
|
|
name: https
|
|
targetPort: 3000
|
|
sessionAffinity: ClientIP
|