feat: migrate to Gitea, productize Helm chart, unify version

- CI: replace GitLab pipeline with Gitea Actions (.gitea/workflows/release.yaml);
  publish .deb/image/hosted/chart to Gitea registries; gate image+hosted on .deb
  (needs + registry pre-check)
- drop connectone/Nexus; apt now from the Gitea Debian registry
- single version source (/VERSION=3.2.8); CI enforces .env/Chart consistency
- restructure: build/->packaging/deb, root Dockerfile->packaging/image,
  hosted/->packaging/hosted, .kube/->charts/freeradius
- Helm chart: documented values, DB password via Secret/$ENV, configurable
  scheduling/persistence; remove plaintext creds and hostPath PV
- gitignore *.env_cnf; docs for hosted (RU)/helm/root + agent KB sync

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Georg K
2026-06-12 05:45:10 +03:00
parent da25848bf8
commit 99e1245ed3
52 changed files with 1079 additions and 287 deletions

View File

@@ -0,0 +1,9 @@
# Patterns to ignore when building Helm packages.
.DS_Store
.git/
.gitignore
*.tmp
*.bak
*.orig
*.swp
README.md.tmpl

View File

@@ -0,0 +1,11 @@
apiVersion: v2
name: freeradius
description: Custom FreeRADIUS 3.2.x with the rlm_raw module, PostgreSQL backend and REST-based dynamic clients.
type: application
# Chart version (semver). Bump on chart changes.
version: 1.2.0
# Application version. Kept in sync with the repo-wide /VERSION by CI.
appVersion: "3.2.8"
home: https://git.ahax86.ru/pub/freeradius
sources:
- https://git.ahax86.ru/pub/freeradius

View File

@@ -0,0 +1,83 @@
# FreeRADIUS Helm chart
Deploys the custom FreeRADIUS 3.2.x build — with the `rlm_raw`
module, PostgreSQL backend and REST-based dynamic clients — to Kubernetes.
The chart is published to the Gitea Helm registry by CI:
`https://git.ahax86.ru/api/packages/pub/helm`
## Prerequisites
- The container image must be published (CI job `build-image`):
`git.ahax86.ru/pub/freeradius:<version>`.
- A reachable PostgreSQL with the RADIUS schema (`radacct`, `radcheck`, `nas`, ...).
- The REST `hotspotter` service (fallback NAS authorization).
- If the image registry is private: a pre-created `docker-registry` Secret,
referenced via `imagePullSecrets`.
## Install
```bash
helm repo add --username <user> --password <token> \
ahax86 https://git.ahax86.ru/api/packages/pub/helm
helm repo update
helm upgrade --install freeradius ahax86/freeradius \
--namespace radius --create-namespace \
--set db.host=postgresql-pgbouncer \
--set db.password=<db-password>
```
Use an existing Secret for the DB password instead of `--set db.password`:
```bash
helm upgrade --install freeradius ahax86/freeradius -n radius \
--set db.existingSecret=freeradius-db --set db.passwordKey=password
```
`db.password` (or `db.existingSecret`) is required — install fails otherwise.
## How config is delivered
- `default` (virtual server) and `rest` (hotspotter) render into a ConfigMap.
- `sql` renders into the same ConfigMap but the password is **not** stored there:
it is read at runtime via `$ENV{DB_PASSWORD}`, injected from the DB Secret.
- The entrypoint copies `/configs/{default,sql,rest}` into `/etc/freeradius`.
- Certificates live on a PVC and survive restarts.
## Values
| Key | Default | Description |
|-----|---------|-------------|
| `replicaCount` | `1` | Pod replicas. |
| `image.repository` | `git.ahax86.ru/pub/freeradius` | Image repo. |
| `image.tag` | `""` | Empty → chart `appVersion`. |
| `image.pullPolicy` | `IfNotPresent` | |
| `imagePullSecrets` | `[]` | Names of pull secrets. |
| `args` | `[freeradius, -f]` | Use `[freeradius, -X]` for debug. |
| `service.type` | `NodePort` | `ClusterIP` / `NodePort` / `LoadBalancer`. |
| `service.authPort` / `service.acctPort` | `31812` / `31813` | UDP ports. |
| `service.authNodePort` / `service.acctNodePort` | `31812` / `31813` | NodePort only. |
| `service.externalTrafficPolicy` | `Local` | NodePort only. |
| `db.host` / `db.port` | `postgresql-pgbouncer` / `6432` | PostgreSQL. |
| `db.user` / `db.name` | `admin` / `radius_db` | |
| `db.password` | `""` | Required unless `db.existingSecret`. |
| `db.existingSecret` / `db.passwordKey` | `""` / `db-password` | Use an existing Secret. |
| `api.host` / `api.port` | `hotspotter` / `8080` | REST hotspotter. |
| `persistence.enabled` | `true` | Cert PVC (else `emptyDir`). |
| `persistence.existingClaim` | `""` | Use a pre-created PVC. |
| `persistence.storageClass` | `""` | |
| `persistence.size` | `32Mi` | |
| `resources` / `nodeSelector` / `tolerations` / `affinity` | `{}` / `{}` / `[]` / `{}` | Scheduling. |
| `env` | `{TZ: UTC}` | Extra container env. |
> Note: the previous deployment pinned `nodeSelector freeradius=enabled` and a
> matching toleration. These are no longer hardcoded — set them via
> `nodeSelector` / `tolerations` if your nodes are tainted.
## Render locally
```bash
helm template freeradius charts/freeradius --set db.password=test | less
helm lint charts/freeradius --set db.password=test
```

View File

@@ -0,0 +1,20 @@
FreeRADIUS ({{ .Chart.AppVersion }}) released as {{ .Release.Name }} in namespace {{ .Release.Namespace }}.
Image: {{ include "freeradius.image" . }}
Service: {{ include "freeradius.fullname" . }} ({{ .Values.service.type }})
auth: UDP {{ .Values.service.authPort }}{{ if eq .Values.service.type "NodePort" }} (nodePort {{ .Values.service.authNodePort }}){{ end }}
acct: UDP {{ .Values.service.acctPort }}{{ if eq .Values.service.type "NodePort" }} (nodePort {{ .Values.service.acctNodePort }}){{ end }}
Backends:
PostgreSQL: {{ .Values.db.host }}:{{ .Values.db.port }} db={{ .Values.db.name }} user={{ .Values.db.user }}
REST (hotspotter): http://{{ .Values.api.host }}:{{ .Values.api.port }}/
{{- if and (not .Values.db.existingSecret) (not .Values.db.password) }}
WARNING: db.password is empty and no db.existingSecret was provided.
Set one of them, e.g.: helm upgrade {{ .Release.Name }} ... --set db.password=<password>
{{- end }}
Check status:
kubectl -n {{ .Release.Namespace }} rollout status deploy/{{ include "freeradius.fullname" . }}
kubectl -n {{ .Release.Namespace }} logs deploy/{{ include "freeradius.fullname" . }}

View File

@@ -0,0 +1,63 @@
{{/* Chart name */}}
{{- define "freeradius.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/* Fully qualified app name */}}
{{- define "freeradius.fullname" -}}
{{- if .Values.fullnameOverride -}}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- $name := default .Chart.Name .Values.nameOverride -}}
{{- if contains $name .Release.Name -}}
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{/* Common labels */}}
{{- define "freeradius.labels" -}}
helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{ include "freeradius.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end -}}
{{/* Selector labels */}}
{{- define "freeradius.selectorLabels" -}}
app.kubernetes.io/name: {{ include "freeradius.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end -}}
{{/* Container image ref (tag defaults to appVersion) */}}
{{- define "freeradius.image" -}}
{{- $tag := .Values.image.tag | default .Chart.AppVersion -}}
{{- printf "%s:%s" .Values.image.repository $tag -}}
{{- end -}}
{{/* Name of the Secret holding the DB password */}}
{{- define "freeradius.dbSecretName" -}}
{{- if .Values.db.existingSecret -}}
{{- .Values.db.existingSecret -}}
{{- else -}}
{{- printf "%s-db" (include "freeradius.fullname" .) -}}
{{- end -}}
{{- end -}}
{{/* Key inside the DB Secret */}}
{{- define "freeradius.dbSecretKey" -}}
{{- .Values.db.passwordKey | default "db-password" -}}
{{- end -}}
{{/* Name of the PVC used for certs */}}
{{- define "freeradius.pvcName" -}}
{{- if .Values.persistence.existingClaim -}}
{{- .Values.persistence.existingClaim -}}
{{- else -}}
{{- printf "%s-certs" (include "freeradius.fullname" .) -}}
{{- end -}}
{{- end -}}

View File

@@ -0,0 +1,168 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "freeradius.fullname" . }}-configs
namespace: {{ .Release.Namespace }}
labels:
{{- include "freeradius.labels" . | nindent 4 }}
data:
default: |
server default {
listen {
type = auth
ipaddr = *
port = {{ .Values.service.authPort }}
limit {
max_connections = 16
lifetime = 0
idle_timeout = 30
}
}
listen {
ipaddr = *
port = {{ .Values.service.acctPort }}
type = acct
limit {
}
}
authorize {
filter_username
preprocess
chap
mschap
digest
suffix
-sql
expiration
logintime
pap
Autz-Type New-TLS-Connection {
ok
}
}
authenticate {
Auth-Type PAP {
pap
}
Auth-Type CHAP {
chap
}
digest
}
preacct {
preprocess
acct_unique
suffix
files
}
accounting {
detail
unix
-sql
exec
attr_filter.accounting_response
}
session {
}
post-auth {
if (session-state:User-Name && reply:User-Name && request:User-Name && (reply:User-Name == request:User-Name)) {
update reply {
&User-Name !* ANY
}
}
update {
&reply: += &session-state:
}
-sql
exec
remove_reply_message_if_eap
Post-Auth-Type REJECT {
-sql
attr_filter.access_reject
remove_reply_message_if_eap
}
Post-Auth-Type Client-Lost {
}
if (EAP-Key-Name && &reply:EAP-Session-Id) {
update reply {
&EAP-Key-Name := &reply:EAP-Session-Id
}
}
}
pre-proxy {
}
post-proxy {
}
}
sql: |
sql {
dialect = "postgresql"
driver = "rlm_sql_${dialect}"
postgresql {
send_application_name = yes
}
server = "{{ .Values.db.host }}"
port = {{ .Values.db.port }}
login = "{{ .Values.db.user }}"
password = "$ENV{DB_PASSWORD}"
radius_db = "{{ .Values.db.name }}"
acct_table1 = "radacct"
acct_table2 = "radacct"
postauth_table = "radpostauth"
authcheck_table = "radcheck"
groupcheck_table = "radgroupcheck"
authreply_table = "radreply"
groupreply_table = "radgroupreply"
usergroup_table = "radusergroup"
delete_stale_sessions = yes
pool {
start = ${thread[pool].start_servers}
min = ${thread[pool].min_spare_servers}
max = ${thread[pool].max_servers}
spare = ${thread[pool].max_spare_servers}
uses = 0
retry_delay = 30
lifetime = 0
idle_timeout = 60
}
client_table = "nas"
group_attribute = "SQL-Group"
$INCLUDE ${modconfdir}/${.:name}/main/${dialect}/queries.conf
}
rest: |
rest {
tls {
}
connect_uri = "http://{{ .Values.api.host }}:{{ .Values.api.port }}/"
authorize {
uri = "${..connect_uri}radius/auth"
method = 'post'
body = 'json'
data = '{"ip": "%{Packet-Src-IP-Address}", "hotspot-group": "%{raw:Called-Station-Id}", "hotspot-id": "%{raw:NAS-Identifier}"}'
force_to = 'json'
auth = 'none'
require_auth = no
timeout = 4.000000
}
pool {
start = ${thread[pool].start_servers}
min = ${thread[pool].min_spare_servers}
max = ${thread[pool].max_servers}
spare = ${thread[pool].max_spare_servers}
uses = 0
retry_delay = 30
lifetime = 0
idle_timeout = 60
}
}

View File

@@ -0,0 +1,93 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "freeradius.fullname" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "freeradius.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "freeradius.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "freeradius.selectorLabels" . | nindent 8 }}
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
containers:
- name: {{ include "freeradius.name" . }}
image: {{ include "freeradius.image" . | quote }}
imagePullPolicy: {{ .Values.image.pullPolicy }}
args: {{ toJson .Values.args }}
env:
- name: MY_POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: {{ include "freeradius.dbSecretName" . }}
key: {{ include "freeradius.dbSecretKey" . }}
{{- range $k, $v := .Values.env }}
- name: {{ $k }}
value: {{ $v | quote }}
{{- end }}
ports:
- name: auth
containerPort: {{ .Values.service.authPort }}
protocol: UDP
- name: acct
containerPort: {{ .Values.service.acctPort }}
protocol: UDP
{{- with .Values.resources }}
resources:
{{- toYaml . | nindent 12 }}
{{- end }}
volumeMounts:
- name: certs
mountPath: /etc/freeradius/certs
- name: configs
mountPath: /configs/default
subPath: default
readOnly: true
- name: configs
mountPath: /configs/sql
subPath: sql
readOnly: true
- name: configs
mountPath: /configs/rest
subPath: rest
readOnly: true
volumes:
- name: configs
configMap:
name: {{ include "freeradius.fullname" . }}-configs
- name: certs
{{- if .Values.persistence.enabled }}
persistentVolumeClaim:
claimName: {{ include "freeradius.pvcName" . }}
{{- else }}
emptyDir: {}
{{- end }}

View File

@@ -0,0 +1,18 @@
{{- if and .Values.persistence.enabled (not .Values.persistence.existingClaim) }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "freeradius.pvcName" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "freeradius.labels" . | nindent 4 }}
spec:
accessModes:
- {{ .Values.persistence.accessMode }}
{{- if .Values.persistence.storageClass }}
storageClassName: {{ .Values.persistence.storageClass | quote }}
{{- end }}
resources:
requests:
storage: {{ .Values.persistence.size | quote }}
{{- end }}

View File

@@ -0,0 +1,12 @@
{{- if not .Values.db.existingSecret }}
apiVersion: v1
kind: Secret
metadata:
name: {{ printf "%s-db" (include "freeradius.fullname" .) }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "freeradius.labels" . | nindent 4 }}
type: Opaque
stringData:
{{ include "freeradius.dbSecretKey" . }}: {{ required "db.password is required (or set db.existingSecret)" .Values.db.password | quote }}
{{- end }}

View File

@@ -0,0 +1,29 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "freeradius.fullname" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "freeradius.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
{{- if eq .Values.service.type "NodePort" }}
externalTrafficPolicy: {{ .Values.service.externalTrafficPolicy }}
{{- end }}
ports:
- name: auth
port: {{ .Values.service.authPort }}
targetPort: auth
protocol: UDP
{{- if eq .Values.service.type "NodePort" }}
nodePort: {{ .Values.service.authNodePort }}
{{- end }}
- name: acct
port: {{ .Values.service.acctPort }}
targetPort: acct
protocol: UDP
{{- if eq .Values.service.type "NodePort" }}
nodePort: {{ .Values.service.acctNodePort }}
{{- end }}
selector:
{{- include "freeradius.selectorLabels" . | nindent 4 }}

View File

@@ -0,0 +1,74 @@
# Default values for the freeradius chart.
# This is a YAML-formatted file. All keys are documented below.
replicaCount: 1
image:
# Container image built by this repo and pushed to the Gitea registry.
repository: git.ahax86.ru/pub/freeradius
# Image tag. Empty -> defaults to the chart appVersion (e.g. 3.2.8).
tag: ""
pullPolicy: IfNotPresent
# Names of pre-created docker-registry secrets used to pull the image.
imagePullSecrets: []
# - name: gitea-registry
nameOverride: ""
fullnameOverride: ""
# Run arguments for the container. Use ["freeradius","-X"] for debug logging.
args:
- freeradius
- -f
# Auth / accounting service. FreeRADIUS uses UDP.
service:
# ClusterIP | NodePort | LoadBalancer
type: NodePort
authPort: 31812
acctPort: 31813
# nodePort values are used only when type: NodePort.
authNodePort: 31812
acctNodePort: 31813
externalTrafficPolicy: Local
# PostgreSQL backend (RADIUS schema: radacct, radcheck, nas, ...).
# The password is injected into the pod as the DB_PASSWORD env var and read by
# FreeRADIUS via $ENV{DB_PASSWORD}; it is never written into a ConfigMap.
db:
host: postgresql-pgbouncer
port: 6432
user: admin
name: radius_db
# Provide the password at install time (never commit a real value), e.g.
# helm install ... --set db.password=...
password: ""
# Or reference an existing Secret instead of db.password:
existingSecret: ""
passwordKey: db-password
# REST fallback authorization (hotspotter): used when a NAS is not in the `nas` table.
api:
host: hotspotter
port: 8080
# Certificate persistence. Certs are generated on first start and must survive
# restarts. Mounted at /etc/freeradius/certs.
persistence:
enabled: true
# Use a pre-created PVC instead of letting the chart create one.
existingClaim: ""
storageClass: ""
accessMode: ReadWriteOnce
size: 32Mi
# Extra environment variables for the container.
env:
TZ: UTC
resources: {}
podAnnotations: {}
nodeSelector: {}
tolerations: []
affinity: {}