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

@@ -1,6 +1,7 @@
.git/ .git/
.idea/ .idea/
.gitea/
*.md *.md
.kube/ charts/
build/ packaging/deb/
.kube/ agent/

View File

@@ -0,0 +1,162 @@
name: release
# Manual release pipeline (Gitea Actions).
#
# Order / gating (".deb is the source of truth, hosted/image must not build without it"):
# prepare ──► build-deb ──► build-image ──► package-helm
# └──► package-hosted
# build-image and package-hosted additionally verify the .deb exists in the
# registry before doing anything (belt-and-suspenders gate).
#
# Required repo secrets:
# PACKAGES_USER Gitea username (token owner)
# PACKAGES_TOKEN Gitea access token with write:package (+ read:package)
on:
workflow_dispatch:
inputs:
rlm_raw_branch:
description: rlm_raw module branch
required: false
default: v3.2.4
concurrency:
group: release
cancel-in-progress: false
env:
REGISTRY: git.ahax86.ru
OWNER: pub
IMAGE: git.ahax86.ru/pub/freeradius
DEB_REGISTRY: https://git.ahax86.ru/api/packages/pub/debian
DEB_DISTRIBUTION: noble
DEB_COMPONENT: main
jobs:
prepare:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.v.outputs.version }}
radius_tag: ${{ steps.v.outputs.radius_tag }}
pkg_version: ${{ steps.v.outputs.pkg_version }}
steps:
- uses: actions/checkout@v4
- id: v
name: Resolve version from /VERSION
run: |
set -euo pipefail
version="$(tr -d ' \n\r' < VERSION)"
radius_tag="release_$(echo "$version" | tr '.' '_')"
pkg_version="${version}+git"
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "radius_tag=$radius_tag" >> "$GITHUB_OUTPUT"
echo "pkg_version=$pkg_version" >> "$GITHUB_OUTPUT"
echo "version=$version tag=$radius_tag pkg=$pkg_version"
- name: Check version consistency
run: |
set -euo pipefail
version="$(tr -d ' \n\r' < VERSION)"
env_tag="$(grep -E '^FREERADIUS_TAG=' packaging/hosted/.env | cut -d= -f2 | tr -d ' \r')"
chart_app="$(grep -E '^appVersion:' charts/freeradius/Chart.yaml | sed -E 's/appVersion:[[:space:]]*"?([^"]+)"?/\1/' | tr -d ' \r')"
[ "$env_tag" = "$version" ] || { echo "hosted/.env FREERADIUS_TAG=$env_tag != VERSION=$version"; exit 1; }
[ "$chart_app" = "$version" ] || { echo "Chart appVersion=$chart_app != VERSION=$version"; exit 1; }
echo "Versions consistent: $version"
build-deb:
needs: [prepare]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build .deb builder image
run: |
docker build packaging/deb -f packaging/deb/Dockerfile \
--build-arg RADIUS_TAG=${{ needs.prepare.outputs.radius_tag }} \
--build-arg RLM_RAW_BRANCH=${{ inputs.rlm_raw_branch }} \
-t freerad-builder:${{ needs.prepare.outputs.version }}
- name: Upload .deb to Gitea Debian registry
env:
DEB_USER: ${{ secrets.PACKAGES_USER }}
DEB_PASS: ${{ secrets.PACKAGES_TOKEN }}
run: |
docker run --rm \
-e DEB_USER -e DEB_PASS \
-e DEB_REGISTRY="${{ env.DEB_REGISTRY }}" \
-e DEB_DISTRIBUTION="${{ env.DEB_DISTRIBUTION }}" \
-e DEB_COMPONENT="${{ env.DEB_COMPONENT }}" \
freerad-builder:${{ needs.prepare.outputs.version }} ./upload.sh
build-image:
needs: [prepare, build-deb]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Verify .deb is published
env:
DEB_USER: ${{ secrets.PACKAGES_USER }}
DEB_PASS: ${{ secrets.PACKAGES_TOKEN }}
run: |
set -euo pipefail
url="${{ env.DEB_REGISTRY }}/dists/${{ env.DEB_DISTRIBUTION }}/${{ env.DEB_COMPONENT }}/binary-amd64/Packages"
curl -fsSL --user "$DEB_USER:$DEB_PASS" "$url" \
| grep -q "Version: ${{ needs.prepare.outputs.pkg_version }}" \
|| { echo "Package ${{ needs.prepare.outputs.pkg_version }} not found in registry"; exit 1; }
- name: Login to container registry
run: echo "${{ secrets.PACKAGES_TOKEN }}" | docker login ${{ env.REGISTRY }} -u "${{ secrets.PACKAGES_USER }}" --password-stdin
- name: Build and push image
run: |
set -euo pipefail
docker build -f packaging/image/Dockerfile \
--build-arg PKG_VERSION=${{ needs.prepare.outputs.pkg_version }} \
-t ${{ env.IMAGE }}:${{ needs.prepare.outputs.version }} \
-t ${{ env.IMAGE }}:latest .
docker push ${{ env.IMAGE }}:${{ needs.prepare.outputs.version }}
docker push ${{ env.IMAGE }}:latest
package-hosted:
needs: [prepare, build-deb]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Verify .deb is published
env:
DEB_USER: ${{ secrets.PACKAGES_USER }}
DEB_PASS: ${{ secrets.PACKAGES_TOKEN }}
run: |
set -euo pipefail
url="${{ env.DEB_REGISTRY }}/dists/${{ env.DEB_DISTRIBUTION }}/${{ env.DEB_COMPONENT }}/binary-amd64/Packages"
curl -fsSL --user "$DEB_USER:$DEB_PASS" "$url" \
| grep -q "Version: ${{ needs.prepare.outputs.pkg_version }}" \
|| { echo "Package ${{ needs.prepare.outputs.pkg_version }} not found in registry"; exit 1; }
- name: Package and upload hosted bundle
env:
PKG_USER: ${{ secrets.PACKAGES_USER }}
PKG_TOKEN: ${{ secrets.PACKAGES_TOKEN }}
run: |
set -euo pipefail
chmod +x packaging/hosted/install.sh packaging/hosted/update.sh
tar -czf hosted.tar.gz -C packaging hosted
ver="${{ needs.prepare.outputs.version }}"
curl -fsSL --user "$PKG_USER:$PKG_TOKEN" \
--upload-file hosted.tar.gz \
"https://${{ env.REGISTRY }}/api/packages/${{ env.OWNER }}/generic/hosted/${ver}/hosted.tar.gz"
package-helm:
needs: [prepare, build-image]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install helm
run: curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
- name: Lint chart
run: helm lint charts/freeradius --set db.password=ci
- name: Package and push chart
env:
PKG_USER: ${{ secrets.PACKAGES_USER }}
PKG_TOKEN: ${{ secrets.PACKAGES_TOKEN }}
run: |
set -euo pipefail
helm package charts/freeradius --app-version "${{ needs.prepare.outputs.version }}" -d dist
chart="$(ls dist/*.tgz)"
curl -fsSL --user "$PKG_USER:$PKG_TOKEN" -X POST \
--upload-file "$chart" \
"https://${{ env.REGISTRY }}/api/packages/${{ env.OWNER }}/helm/api/charts"

3
.gitignore vendored
View File

@@ -1,3 +1,6 @@
.idea/ .idea/
pkg/ pkg/
bin/ bin/
# Local secrets for the hosted install — never commit (only *.env_cnf_example is tracked)
*.env_cnf

View File

@@ -1,48 +0,0 @@
variables:
CI_REGISTRY: registry.connectone.pro
CONTAINER_IMAGE: ${CI_REGISTRY}/${CI_PROJECT_PATH}:${CI_COMMIT_REF_NAME}_${CI_COMMIT_SHORT_SHA}
RADIUS_TAG: "release_3_2_7"
stages:
- release
- release-deb
- release-hosted
release:
stage: release
image: docker:latest
before_script:
- docker login -u ${CI_REGISTRY_USER} -p ${CI_REGISTRY_PASSWORD} ${CI_REGISTRY}
script:
- docker build -t ${CONTAINER_IMAGE} .
- docker push ${CONTAINER_IMAGE}
when: manual
only:
- main
release-deb:
stage: release-deb
image: docker:latest
variables:
DOCKER_BUILDKIT: 1
script:
- cd build
- docker build --build-arg RADIUS_TAG=${RADIUS_TAG} . -t freerad_builder:3.2.x -f Dockerfile
- docker run -e DEB_USER=${DEB_USER} -e DEB_PASS="${DEB_PASS}" freerad_builder:3.2.x ./upload.sh
when: manual
only:
- main
release-hosted:
stage: release-hosted
image: alpine:latest
variables:
VERSION: "${RADIUS_TAG}"
script:
- apk add --no-cache curl
- chmod a+x hosted/install.sh hosted/update.sh
- tar -czvf hosted.tar.gz hosted
- 'curl --header "JOB-TOKEN: $CI_JOB_TOKEN" --upload-file hosted.tar.gz "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/hosted/${VERSION}/hosted.tar.gz"'
when: manual
only:
- main

View File

@@ -1,4 +0,0 @@
apiVersion: v2
name: freeradius
version: 1.0.1
appVersion: 3.2.0

View File

@@ -1,8 +0,0 @@
apiVersion: v1
kind: Secret
metadata:
name: {{.Release.Name}}-registry-key
namespace: {{.Release.Namespace}}
data:
.dockerconfigjson: "{{.Values.registryAuthBase64}}"
type: kubernetes.io/dockerconfigjson

View File

@@ -1,70 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{.Release.Name}}
namespace: {{.Release.Namespace}}
spec:
replicas: 1
selector:
matchLabels:
app: {{.Release.Name}}
template:
metadata:
labels:
app: {{.Release.Name}}
spec:
nodeSelector:
freeradius: enabled
tolerations:
- key: freeradius
operator: Equal
value: enabled
effect: NoSchedule
imagePullSecrets:
- name: {{.Release.Name}}-registry-key
{{/* securityContext:*/}}
{{/* runAsUser: 108*/}}
{{/* runAsGroup: 112*/}}
{{/* fsGroup: 108*/}}
containers:
- name: {{.Release.Name}}
image: {{.Values.image.name}}
imagePullPolicy: {{.Values.image.pullPolicy}}
env:
- name: MY_POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: TZ
value: UTC
- name: HOSTNAME
value: freeradius
args: ["freeradius", "-X"]
ports:
- containerPort: {{.Values.ports.main}}
- containerPort: {{.Values.ports.second}}
volumeMounts:
- mountPath: /etc/freeradius/certs
name: freeradius-cert-data
readOnly: false
- name: configs-volume
mountPath: "/configs/default"
subPath: default
readOnly: true
- name: configs-volume
mountPath: "/configs/sql"
subPath: sql
readOnly: true
- name: configs-volume
mountPath: "/configs/rest"
subPath: rest
readOnly: true
volumes:
- name: configs-volume
configMap:
name: {{ .Release.Name }}-configs
defaultMode: 488
- name: freeradius-cert-data
persistentVolumeClaim:
claimName: freeradius-data-claim

View File

@@ -1,19 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: {{.Release.Name}}-external
namespace: {{.Release.Namespace}}
spec:
type: NodePort
externalTrafficPolicy: Local
ports:
- port: {{.Values.ports.main}}
nodePort: {{.Values.ports.main}}
protocol: UDP
name: freerad-first
- port: {{.Values.ports.second}}
nodePort: {{.Values.ports.second}}
protocol: UDP
name: freerad-second
selector:
app: {{ .Chart.Name }}

View File

@@ -1,26 +0,0 @@
apiVersion: v1
kind: PersistentVolume
metadata:
name: freeradius-data-disk
namespace: {{.Release.Namespace}}
spec:
capacity:
storage: 10Mi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
hostPath:
path: {{.Values.storageBase}}/{{.Chart.Name}}
---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: freeradius-data-claim
namespace: {{.Release.Namespace}}
spec:
volumeName: freeradius-data-disk
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Mi

View File

@@ -1,20 +0,0 @@
image:
name: ${CONTAINER_IMAGE}
pullPolicy: "IfNotPresent"
registryAuthBase64: ${REGISTRY_AUTH}
storageBase: ${STORAGE_BASE}
ports:
main: 31812
second: 31813
db:
host: "postgresql-pgbouncer"
port: 6432
user: "admin"
password: "radius_db"
name: "radius_db"
api:
host: "hotspotter"
port: 8080

60
AGENTS.md Normal file
View File

@@ -0,0 +1,60 @@
# AGENTS.md
## Что это
Репозиторий упаковки и развёртывания кастомной сборки FreeRADIUS 3.2.x для
hotspot/captive-portal сценариев. Прикладного кода нет: это сборочные Dockerfile,
shell-скрипты, Helm/k8s-манифесты и
конфиги FreeRADIUS. Профиль каркаса: infra-lite.
Главный контекст: [agent/INDEX.md](agent/INDEX.md). Читать перед любыми изменениями.
Сборка и поставка - через Gitea (`git.ahax86.ru`, owner `pub`): Debian-реестр для .deb,
container registry для образа, Helm-реестр для чарта, generic-пакет для hosted-бандла.
Nexus и GitLab больше не используются.
## Порядок чтения в начале сессии
1. `AGENTS.md` (этот файл).
2. `agent/INDEX.md` - назначение, артефакты, режимы развёртывания, версии, риски.
## Структура репозитория
```
VERSION ← единый источник версии FreeRADIUS (3.2.8)
.gitea/workflows/release.yaml ← ручной релиз (Gitea Actions): prepare/build-deb/build-image/package-hosted/package-helm
packaging/
deb/ ← сборка .deb из исходников FreeRADIUS + rlm_raw, upload в Debian-реестр Gitea
image/ ← рантайм-образ (Dockerfile + entrypoint.sh + local-noble.list); ставит .deb, печёт конфиг
hosted/ ← bare-metal/VM установка (install.sh, update.sh, systemd, cron, config)
config/ ← radiusd.conf, dictionary, clients.conf, mods-enabled/*, sites-enabled/*
.env / .env_cnf* ← версия пакета и подстановки (хосты, порты, креды БД)
charts/
freeradius/ ← продуктовый Helm-чарт (Deployment, ConfigMap, Secret, PVC, Service)
```
## Инструменты и команды
- Сборка/выкладка - только через Gitea Actions (`.gitea/workflows/release.yaml`, `workflow_dispatch`).
- Версия задаётся в `VERSION`; CI выводит из неё тег/пин/appVersion и падает при рассинхроне.
- Локальная сборка .deb: `docker build packaging/deb -f packaging/deb/Dockerfile --build-arg RADIUS_TAG=release_3_2_8 --build-arg RLM_RAW_BRANCH=v3.2.4 -t freerad-builder:3.2.8`
- Локальная сборка образа: `docker build -f packaging/image/Dockerfile --build-arg PKG_VERSION=3.2.8+git -t freeradius:3.2.8 .` (контекст = корень).
- Тест hosted: `cd packaging/hosted && docker build . -t hosted:test -f Dockerfile`
- Установка на ноду (bare-metal): `packaging/hosted/install.sh` (root), обновление версии: `packaging/hosted/update.sh`.
- Чарт: `helm lint charts/freeradius --set db.password=test`, `helm template charts/freeradius --set db.password=test`.
- Линтеров/тестов кода в репозитории нет.
## Правила репо
1. Ничего не публиковать (push, PR, CI-стейдж, деплой) без явной команды пользователя.
2. `git add` строго поимённо. Никогда `git add .` или `git add -A`.
3. `.env` / `.env_cnf` не читать целиком и не коммитить - там креды БД. Проверять
переменную через grep по `^VAR=`. `.env_cnf` хранит секреты и не должен попадать в git.
4. Деструктивные действия с прод-нодами, БД и k8s - только с явного подтверждения.
5. Изменился конфиг, версия, хост или инвариант - синхронно отразить в `agent/INDEX.md`.
6. Долгоживущие решения фиксируются в `agent/INDEX.md`. Журнал сессий не ведётся по умолчанию.
7. Документация краткая и строгая: без галлюцинаций, факты берём из кода и конфигов.
## Язык
Отвечать по-русски, кратко, без длинных тире.

1
CLAUDE.md Normal file
View File

@@ -0,0 +1 @@
@AGENTS.md

View File

@@ -1,30 +0,0 @@
FROM ubuntu:24.04
WORKDIR /tmp
ARG out_dir=/etc/freeradius
COPY local-noble.list /etc/apt/sources.list.d/ubuntu.sources
RUN apt-get update && apt-get install -y apt-transport-https ca-certificates curl software-properties-common gpg
RUN curl -fsSL http://connectone.pro/gpg | gpg --dearmor -o /usr/share/keyrings/connectone.gpg && \
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/connectone.gpg] \
https://nexus.connectone.pro/repository/radius bionic main" | tee /etc/apt/sources.list.d/connectone.list \
> /dev/null
RUN apt-get update && apt-get install -y libfreeradius3=3.2.8+git freeradius-common=3.2.8+git \
freeradius-postgresql=3.2.8+git freeradius-rest=3.2.8+git freeradius-utils=3.2.8+git freeradius=3.2.8+git && \
apt autoremove --purge -y && apt-get clean
COPY hosted /tmp/hosted
RUN cp hosted/config/radiusd.conf $out_dir/radiusd.conf && \
cp hosted/config/dictionary $out_dir/dictionary && \
cp hosted/config/clients.conf $out_dir/clients.conf && \
cp hosted/config/mods-enabled/raw $out_dir/mods-enabled/raw && \
cp hosted/config/sites-enabled/dynamic-clients $out_dir/sites-enabled/dynamic-clients && \
rm -f /etc/freeradius/sites-enabled/inner-tunnel && \
rm -f /etc/freeradius/mods-enabled/eap \
RUN rm -f /etc/freeradius/mods-enabled/delay && \
ln -s $out_dir/mods-available/delay $out_dir/mods-enabled/delay
ADD entrypoint.sh /usr/bin/entrypoint.sh
RUN chmod a+x /usr/bin/entrypoint.sh && chmod -R 750 $out_dir && chown -R freerad:root $out_dir && \
mv /etc/freeradius/certs /tmp && \
rm -rf /tmp/hosted
WORKDIR /etc/freeradius
ENTRYPOINT ["/usr/bin/entrypoint.sh"]
CMD ["freeradius", "-f"]

View File

@@ -1,20 +1,87 @@
# freeradius # FreeRADIUS
build: Packaging and delivery of a custom FreeRADIUS 3.2.x build for
```bash hotspot / captive-portal scenarios. There is no application code here — the repo
cd build builds upstream FreeRADIUS with the extra `rlm_raw` module and ships it to two
docker build . -t freerad_builder:3.2.x -f Dockerfile_build runtimes (bare-metal and Kubernetes) via a Gitea-hosted registry.
docker run -e DEB_USER=${DEB_USER} DEB_PASS="${DEB_PASS}" freerad_builder:3.2.x ./upload.sh
## Single source of version truth
[`VERSION`](VERSION) holds the FreeRADIUS release (`3.2.8`). CI derives everything
from it: the build tag `release_3_2_8`, the package pin `3.2.8+git`, the chart
`appVersion`. CI fails if `packaging/hosted/.env` or `charts/freeradius/Chart.yaml`
drift from `VERSION`.
## Layout
```
VERSION single source of the FreeRADIUS version
.gitea/workflows/release.yaml manual release pipeline (Gitea Actions)
packaging/
deb/ build the .deb from FreeRADIUS source + rlm_raw, upload to registry
image/ runtime container image (installs the .deb, bakes configs)
hosted/ bare-metal / VM install (install.sh, update.sh, config, cron)
charts/
freeradius/ productized Helm chart
``` ```
label nodes: ## Artifacts (Gitea, owner `pub`)
```bash
kubectl label node connect-prod-0 freeradius=enabled | Artifact | Registry |
|----------|----------|
| `.deb` packages | `https://git.ahax86.ru/api/packages/pub/debian` (`noble main`) |
| Container image | `git.ahax86.ru/pub/freeradius:<version>` |
| Hosted bundle | `.../api/packages/pub/generic/hosted/<version>/hosted.tar.gz` |
| Helm chart | `https://git.ahax86.ru/api/packages/pub/helm` |
## Release flow (`.gitea/workflows/release.yaml`, manual)
```
prepare ──► build-deb ──► build-image ──► package-helm
└──► package-hosted
``` ```
remove The `.deb` is the prerequisite: `build-image` and `package-hosted` both `needs:
```bash build-deb` **and** verify the pinned version exists in the Debian registry
before running. Trigger from Gitea → Actions → `release`*Run workflow*.
sudo apt-mark unhold libfreeradius3 freeradius-common freeradius-postgresql freeradius-rest freeradius-utils freeradius ### CI setup (Gitea)
sudo dpkg --force-overwrite --purge freeradius-common freeradius-config freeradius-python3 freeradius libfreeradius3 freeradius-postgresql freeradius-rest freeradius-utils && sudo rm -rf /etc/freeradius && sudo rm -rf /usr/lib/freeradius
1. **Runner.** Enable Actions for the repo and register an `ubuntu-latest` runner
(`act_runner`) with Docker available. If your runner uses a different label,
change `runs-on` in the workflow.
2. **Access token.** Gitea → avatar → *Settings**Applications*
*Manage Access Tokens**Generate New Token*. Scopes: `write:package` and
`read:package`. Copy the token (shown once).
3. **Repo secrets.** Repo → *Settings**Actions**Secrets**Add Secret*:
- `PACKAGES_USER` — your Gitea username (the token owner).
- `PACKAGES_TOKEN` — the token from step 2.
These secrets are used to push to the container / Debian / generic / Helm
registries and to read the Debian `Packages` index in the gate check.
## Local builds (no upload)
```bash
# .deb builder image
docker build packaging/deb -f packaging/deb/Dockerfile \
--build-arg RADIUS_TAG=release_3_2_8 --build-arg RLM_RAW_BRANCH=v3.2.4 \
-t freerad-builder:3.2.8
# runtime image (context = repo root; pulls .deb from the Debian registry)
docker build -f packaging/image/Dockerfile --build-arg PKG_VERSION=3.2.8+git -t freeradius:3.2.8 .
# chart
helm lint charts/freeradius --set db.password=test
helm template freeradius charts/freeradius --set db.password=test
``` ```
## Deployment docs
- Bare-metal / VM: [packaging/hosted/README.md](packaging/hosted/README.md)
- Kubernetes (Helm): [charts/freeradius/README.md](charts/freeradius/README.md)
## Internals
See [agent/INDEX.md](agent/INDEX.md) for invariants, hosts/ports, backends and
known constraints.

1
VERSION Normal file
View File

@@ -0,0 +1 @@
3.2.8

104
agent/INDEX.md Normal file
View File

@@ -0,0 +1,104 @@
# FreeRADIUS - обзор инфраструктуры
Профиль: infra-lite. Бизнес-логику самого FreeRADIUS здесь не описываем.
## Назначение
Сборка и поставка кастомного FreeRADIUS 3.2.x для hotspot/captive-portal сценариев.
Репозиторий не содержит сервиса - он упаковывает апстримный FreeRADIUS с
доп. модулем `rlm_raw` и накладывает конфиги под две среды выполнения.
## Версия - единый источник
`VERSION` (корень) = `3.2.8`. Из него CI выводит:
- `RADIUS_TAG=release_3_2_8` (тег исходников FreeRADIUS для сборки .deb),
- пин пакетов `3.2.8+git` (образ и hosted),
- `appVersion` чарта.
CI (`prepare`) падает, если `packaging/hosted/.env` (`FREERADIUS_TAG`) или
`charts/freeradius/Chart.yaml` (`appVersion`) разошлись с `VERSION`. Ветка
`rlm_raw` - отдельный параметр (`workflow_dispatch` input, дефолт `v3.2.4`).
## Артефакты (Gitea, owner `pub`)
| Артефакт | Реестр |
|----------|--------|
| `.deb` | `https://git.ahax86.ru/api/packages/pub/debian` (distribution `noble`, component `main`) |
| Образ | `git.ahax86.ru/pub/freeradius:<version>` (+ `latest`) |
| Hosted-бандл | `.../api/packages/pub/generic/hosted/<version>/hosted.tar.gz` |
| Helm-чарт | `https://git.ahax86.ru/api/packages/pub/helm` |
Соглашение: локальный alias helm-репо - `ahax86` (`helm repo add ahax86 ...`).
## Релизный поток (`.gitea/workflows/release.yaml`, `workflow_dispatch`)
```
prepare ──► build-deb ──► build-image ──► package-helm
└──► package-hosted
```
- `.deb` - источник истины: `build-image` и `package-hosted` имеют `needs: build-deb`
И дополнительно проверяют, что версия `3.2.8+git` уже опубликована в Debian-реестре
(GET `dists/noble/main/binary-amd64/Packages`), иначе fail.
- Авторизация: репо-секреты `PACKAGES_USER` / `PACKAGES_TOKEN` (Gitea token, scope
`write:package`).
- Раннер: `ubuntu-latest` с доступным Docker.
## Режимы развёртывания
- Kubernetes (`charts/freeradius/`): продуктовый Helm-чарт. Образ из `build-image`.
Конфиги `default`/`rest` - в ConfigMap; `sql` - тоже в ConfigMap, но пароль БД не
хранится там, а подставляется в рантайме через `$ENV{DB_PASSWORD}` из Secret
(`db.password` или `db.existingSecret`). `entrypoint.sh` копирует `/configs/*` в
`/etc/freeradius`. Сертификаты на PVC (`persistence`). nodeSelector/tolerations -
конфигурируемые (по умолчанию пусто). Установка - из Helm-реестра Gitea.
- Hosted / bare-metal (`packaging/hosted/install.sh`, root): ставит запиненные .deb из
Debian-реестра Gitea (`apt-mark hold`), подставляет хосты/порты/креды в конфиги через
`sed` из `.env_cnf`, ставит systemd-юнит и cron на ежемесячный перевыпуск сертификатов
(`cron/radius-renew.sh`). `update.sh` бампит версию пакета (`unhold` -> install -> hold).
Для приватного реестра - `DEB_AUTH="user:token"`.
## Внешние зависимости и хосты
- Gitea: `git.ahax86.ru`, owner `pub`. Образы - container registry, .deb - Debian-реестр,
чарт - Helm-реестр, hosted - generic-пакет. GPG-ключ apt-репо:
`https://git.ahax86.ru/api/packages/pub/debian/repository.key`.
- PostgreSQL `radius_db` (схема RADIUS: `radacct`, `radcheck`, `nas`, ...). В k8s -
`postgresql-pgbouncer:6432`, юзер `admin`; в hosted - из `.env_cnf`.
- REST API `hotspotter` (k8s: `hotspotter:8080`, эндпоинт `radius/auth`). Используется
как fallback-авторизация NAS, когда клиента нет в таблице `nas`.
## Сетевые порты
- Auth/acct: 31812 / 31813 (см. site `default`, `values.yaml` `service.*`, `.env_cnf`).
- В k8s публикуются Service'ом (по умолчанию NodePort 31812/31813).
## Инварианты
- Клиенты (NAS) резолвятся динамически: site `dynamic-clients` ищет NAS по `shortname`
(= `NAS-Identifier`) в таблице `nas`; если нет - идёт `rest` к `hotspotter`. Статический
`clients.conf` минимален.
- Включён модуль `delay` (delay_reject); удалены `eap`, `inner-tunnel`, `proxy_rate_limit`
(синхронно в образе и hosted).
- Модуль `rlm_raw` обязателен: конфиги используют `%{raw:...}` (Called-Station-Id,
NAS-Identifier). Без него сборка/конфиг не работают.
- Сертификаты в k8s переживают рестарты через PVC; `entrypoint.sh` копирует дефолтные
только если каталог `certs` пуст.
## Риски и known warts
- Образ при сборке тянет .deb из Debian-реестра Gitea - предполагается публичный read для
owner `pub`. Если реестр приватный, сборку образа надо чинить (read-токен/buildkit secret),
а на нодах нужен `DEB_AUTH` для apt.
- Реестр может отклонить повторную заливку той же версии .deb (релиз - переиздание версии
требует bump `VERSION`).
- Пароль БД: в hosted - в `.env_cnf` (gitignored, `*.env_cnf`); в k8s - в Secret. В git не
попадает.
## Рабочие правила
- Менять конфиги/версии осознанно: одно изменение часто нужно отразить и в чарте
(`charts/freeradius/`), и в hosted (`packaging/hosted/`), и в образе (`packaging/image/`).
- Версию менять только в `VERSION` (плюс синхронно `.env`/`Chart.yaml`, иначе CI упадёт).
- Прод-выкладка только вручную через Gitea Actions и только по команде пользователя.
- Долгоживущие решения дописывать сюда. Держать обзор кратким и актуальным.

View File

@@ -1,9 +0,0 @@
#!/bin/bash
set -e
DEB_REPO=${DEB_REPO:-"https://nexus.connectone.pro/repository/radius/"}
for file in *.deb
do
curl -u "${DEB_USER}:${DEB_PASS}" -H "Content-Type: multipart/form-data" --data-binary "@./$file" "${DEB_REPO}"
done; # file

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

@@ -1,17 +1,17 @@
apiVersion: v1 apiVersion: v1
kind: ConfigMap kind: ConfigMap
metadata: metadata:
name: {{ .Release.Name }}-configs name: {{ include "freeradius.fullname" . }}-configs
namespace: {{ .Release.Namespace }} namespace: {{ .Release.Namespace }}
labels: labels:
app: clickhouse-operator {{- include "freeradius.labels" . | nindent 4 }}
data: data:
default: | default: |
server default { server default {
listen { listen {
type = auth type = auth
ipaddr = * ipaddr = *
port = {{.Values.ports.main}} port = {{ .Values.service.authPort }}
limit { limit {
max_connections = 16 max_connections = 16
lifetime = 0 lifetime = 0
@@ -21,7 +21,7 @@ data:
listen { listen {
ipaddr = * ipaddr = *
port = {{.Values.ports.second}} port = {{ .Values.service.acctPort }}
type = acct type = acct
limit { limit {
@@ -99,7 +99,6 @@ data:
} }
} }
sql: | sql: |
sql { sql {
dialect = "postgresql" dialect = "postgresql"
@@ -111,7 +110,7 @@ data:
server = "{{ .Values.db.host }}" server = "{{ .Values.db.host }}"
port = {{ .Values.db.port }} port = {{ .Values.db.port }}
login = "{{ .Values.db.user }}" login = "{{ .Values.db.user }}"
password = "{{.Values.db.password}}" password = "$ENV{DB_PASSWORD}"
radius_db = "{{ .Values.db.name }}" radius_db = "{{ .Values.db.name }}"
acct_table1 = "radacct" acct_table1 = "radacct"
@@ -167,4 +166,3 @@ data:
idle_timeout = 60 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: {}

View File

@@ -1,14 +0,0 @@
# hosted
to test
build
```bash
docker build . -t hosted:test -f Dockerfile
```
run
```bash
docker run -ti -p 1812:1812/UDP -p 1813:1813/UDP hosted:test /bin/bash
```

View File

@@ -10,8 +10,10 @@ COPY local-noble.list /etc/apt/sources.list.d/ubuntu.sources
RUN apt-get update && apt-get install -y devscripts equivs git quilt gcc RUN apt-get update && apt-get install -y devscripts equivs git quilt gcc
# #
# Setup build envs # Setup build envs.
ARG RADIUS_TAG="release_3_2_5" # Defaults match the repo-wide /VERSION (3.2.8). CI overrides RADIUS_TAG
# from VERSION; RLM_RAW_BRANCH is the rlm_raw module branch for this release.
ARG RADIUS_TAG="release_3_2_8"
ARG RLM_RAW_BRANCH="v3.2.4" ARG RLM_RAW_BRANCH="v3.2.4"
ENV RADIUS_REPO="https://github.com/FreeRADIUS/freeradius-server.git" ENV RADIUS_REPO="https://github.com/FreeRADIUS/freeradius-server.git"

41
packaging/deb/upload.sh Normal file
View File

@@ -0,0 +1,41 @@
#!/bin/bash
#
# Uploads built .deb packages to a Gitea Debian package registry.
#
# Required env:
# DEB_USER - Gitea username (token owner)
# DEB_PASS - Gitea access token with the write:package scope
#
# Optional env (defaults target git.ahax86.ru / owner "pub"):
# DEB_REGISTRY - registry base URL
# DEB_DISTRIBUTION - apt distribution (default: noble)
# DEB_COMPONENT - apt component (default: main)
#
set -euo pipefail
DEB_REGISTRY="${DEB_REGISTRY:-https://git.ahax86.ru/api/packages/pub/debian}"
DEB_DISTRIBUTION="${DEB_DISTRIBUTION:-noble}"
DEB_COMPONENT="${DEB_COMPONENT:-main}"
if [ -z "${DEB_USER:-}" ] || [ -z "${DEB_PASS:-}" ]; then
echo "DEB_USER and DEB_PASS must be set" >&2
exit 1
fi
upload_url="${DEB_REGISTRY}/pool/${DEB_DISTRIBUTION}/${DEB_COMPONENT}/upload"
shopt -s nullglob
debs=(*.deb)
if [ "${#debs[@]}" -eq 0 ]; then
echo "No .deb files found to upload" >&2
exit 1
fi
for file in "${debs[@]}"; do
echo "Uploading ${file} -> ${upload_url}"
curl -fsSL --user "${DEB_USER}:${DEB_PASS}" \
--upload-file "./${file}" \
"${upload_url}"
done
echo "Uploaded ${#debs[@]} package(s)"

120
packaging/hosted/README.md Normal file
View File

@@ -0,0 +1,120 @@
# Hosted установка (bare-metal / VM)
## Для чего этот репозиторий
Это сборка и поставка кастомного **FreeRADIUS 3.2.x** для hotspot / captive-portal
сценариев. Прикладного кода здесь нет: репозиторий собирает апстримный FreeRADIUS с
дополнительным модулем `rlm_raw`, накладывает конфиги и доставляет результат в двух
видах - bare-metal (этот каталог) и Kubernetes (Helm-чарт в `charts/freeradius`).
## Как это работает
1. Точки доступа / hotspot-шлюзы (NAS) шлют RADIUS-запросы. Статически клиенты **не**
прописаны - они резолвятся динамически. `clients.conf` объявляет сеть `0.0.0.0/0`
как динамическую (`client dynamic`).
2. Когда приходит пакет от неизвестного NAS, FreeRADIUS вызывает виртуальный сервер
`dynamic_clients` (`sites-enabled/dynamic-clients`). Тот ищет NAS по `NAS-Identifier`
в таблице `nas` PostgreSQL: если запись есть - берёт из БД `secret`/`shortname` и
определяет клиента на лету; если нет - идёт в REST к сервису `hotspotter`.
3. Авторизация абонента идёт в site `default` через SQL (схема RADIUS: `radcheck`,
`radreply`, `radacct`, ...) и/или REST. Hotspotter получает `hotspot-group`
(= `Called-Station-Id`) и `hotspot-id` (= `NAS-Identifier`) и решает, пускать ли.
4. Профиль урезан под задачу: отключены `eap`, `inner-tunnel`, `proxy_rate_limit`,
включён `delay_reject`.
## Зачем нужен rlm_raw
`NAS-Identifier` и `Called-Station-Id` - строковые RADIUS-атрибуты, в которые
hotspot-шлюзы кладут идентификатор точки/группы. Это значение должно совпадать
**байт-в-байт** с `nas.shortname` в БД и с записями в hotspotter.
Штатный xlat `%{NAS-Identifier}` отдаёт значение уже после разбора и нормализации
FreeRADIUS (атрибут декодируется по словарю, к строке применяется обработка). Для
произвольных байтов и нестандартных форматов это меняет значение и ломает поиск.
Модуль `rlm_raw` добавляет xlat `%{raw:...}`, который возвращает атрибут ровно так,
как его прислал NAS, без перекодирования. Все ключевые конфиги (`dynamic-clients`,
`mods-enabled/rest`) построены на `%{raw:Called-Station-Id}` и `%{raw:NAS-Identifier}`,
поэтому модуль обязателен - без него конфиг просто не загрузится. Именно поэтому
FreeRADIUS пересобирается из исходников с `rlm_raw`, а не ставится из стандартного apt.
## Что в этом каталоге
| Файл | Назначение |
|------|------------|
| `.env` | Версия пакета: `FREERADIUS_TAG` (ставится как `<tag>+git`). |
| `.env_cnf_example` | Шаблон для `.env_cnf`. Скопировать и заполнить. |
| `.env_cnf` | Локальные секреты/хосты. **В git не попадает** (gitignore). |
| `config/` | radiusd.conf, dictionary, clients.conf, mods/sites. |
| `install.sh` | Первичная установка (root). |
| `update.sh` | Смена версии пакета (root). |
| `cron/radius-renew.sh` | Ежемесячный перевыпуск сертификатов. |
Этот бандл публикуется CI как generic-пакет:
`https://git.ahax86.ru/api/packages/pub/generic/hosted/<version>/hosted.tar.gz`
## Требования
- Ubuntu 24.04 (noble), root.
- Доступ к `git.ahax86.ru`, к PostgreSQL и к REST-сервису `hotspotter`.
- Нужная версия `.deb` уже опубликована в Debian-реестре Gitea (CI собирает её первой).
## Установка
```bash
cp .env_cnf_example .env_cnf
$EDITOR .env_cnf # заполнить хосты/порты/креды
sudo ./install.sh
```
`install.sh` подключает Debian-реестр Gitea, ставит запиненные пакеты (`apt-mark hold`),
рендерит `sites-enabled/default`, `mods-enabled/rest` и `mods-enabled/sql` из `.env_cnf`,
копирует статические конфиги, включает `delay_reject`, ставит systemd-юнит и cron.
Переменные `.env_cnf`:
| Переменная | Смысл |
|------------|-------|
| `FR_MAIN_PORT` / `FR_ACC_PORT` | UDP-порты auth / accounting. |
| `FR_API_HOST` / `FR_API_PORT` | Endpoint REST hotspotter. |
| `FR_PG_HOST` / `FR_PG_PORT` | Хост / порт PostgreSQL. |
| `FR_PG_USER` / `FR_PG_PASS` / `FR_PG_DB` | Креды / база PostgreSQL. |
### Приватный реестр
Если реестр `pub` не публичный, перед запуском задайте токен:
```bash
export DEB_AUTH="<gitea-user>:<token-с-read:package>"
sudo -E ./install.sh
```
Прочие переопределения (значения по умолчанию):
`DEB_REGISTRY=https://git.ahax86.ru/api/packages/pub/debian`,
`DEB_DISTRIBUTION=noble`, `DEB_COMPONENT=main`.
## Смена версии
```bash
# поменять FREERADIUS_TAG в .env, затем:
sudo ./update.sh
```
`update.sh`: `apt-mark unhold` → установка нужной версии → `apt-mark hold` → перезапуск.
## Удаление
```bash
sudo apt-mark unhold libfreeradius3 freeradius-common freeradius-postgresql \
freeradius-rest freeradius-utils freeradius
sudo apt-get purge -y freeradius freeradius-common freeradius-config \
freeradius-python3 libfreeradius3 freeradius-postgresql freeradius-rest freeradius-utils
sudo rm -rf /etc/freeradius /usr/lib/freeradius
```
## Проверка в Docker
```bash
docker build . -t hosted:test -f Dockerfile
docker run -ti -p 1812:1812/udp -p 1813:1813/udp hosted:test /bin/bash
```

View File

@@ -46,7 +46,6 @@ rest {
# #
# If you wish to disable this pre-caching and reachability check, # If you wish to disable this pre-caching and reachability check,
# comment out the configuration item below. # comment out the configuration item below.
#connect_uri = "http://connectone.me:31668"
connect_uri = "http://#API_HOST#:#API_PORT#/" connect_uri = "http://#API_HOST#:#API_PORT#/"
# #

View File

@@ -14,13 +14,27 @@ out_dir=/etc/freeradius
echo "Installing packages..." echo "Installing packages..."
apt-get update apt-get update
apt-get install -y apt-transport-https ca-certificates curl software-properties-common apt-get install -y apt-transport-https ca-certificates curl
echo "Installing freeradius..." echo "Registering Gitea Debian registry..."
curl -fsSL http://connectone.pro/gpg | gpg --dearmor -o /usr/share/keyrings/connectone.gpg && \ DEB_REGISTRY="${DEB_REGISTRY:-https://git.ahax86.ru/api/packages/pub/debian}"
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/connectone.gpg] \ DEB_DISTRIBUTION="${DEB_DISTRIBUTION:-noble}"
https://nexus.connectone.pro/repository/radius bionic main" | tee /etc/apt/sources.list.d/connectone.list \ DEB_COMPONENT="${DEB_COMPONENT:-main}"
> /dev/null
install -d /etc/apt/keyrings
curl -fsSL "${DEB_REGISTRY}/repository.key" -o /etc/apt/keyrings/gitea-pub.asc
apt_list=/etc/apt/sources.list.d/gitea-freeradius.list
if [ -n "${DEB_AUTH:-}" ]; then
# Private registry: embed "user:token" in the source URL (file kept root-only).
apt_uri="${DEB_REGISTRY%%://*}://${DEB_AUTH}@${DEB_REGISTRY#*://}"
echo "deb [signed-by=/etc/apt/keyrings/gitea-pub.asc] ${apt_uri} ${DEB_DISTRIBUTION} ${DEB_COMPONENT}" >"$apt_list"
chmod 600 "$apt_list"
else
echo "deb [signed-by=/etc/apt/keyrings/gitea-pub.asc] ${DEB_REGISTRY} ${DEB_DISTRIBUTION} ${DEB_COMPONENT}" >"$apt_list"
fi
echo "Installing freeradius ${FREERADIUS_TAG}..."
apt-get update && apt-get install -y libfreeradius3=${FREERADIUS_TAG}+git freeradius-common=${FREERADIUS_TAG}+git \ apt-get update && apt-get install -y libfreeradius3=${FREERADIUS_TAG}+git freeradius-common=${FREERADIUS_TAG}+git \
freeradius-postgresql=${FREERADIUS_TAG}+git freeradius-rest=${FREERADIUS_TAG}+git \ freeradius-postgresql=${FREERADIUS_TAG}+git freeradius-rest=${FREERADIUS_TAG}+git \
freeradius-utils=${FREERADIUS_TAG}+git freeradius=${FREERADIUS_TAG}+git freeradius-utils=${FREERADIUS_TAG}+git freeradius=${FREERADIUS_TAG}+git

View File

@@ -0,0 +1,55 @@
# Runtime image for the custom FreeRADIUS build.
# Installs the pre-built .deb packages from the Gitea Debian registry and bakes
# the shared configs. Build context MUST be the repo root:
# docker build -f packaging/image/Dockerfile -t <ref> .
#
# Version is pinned via PKG_VERSION (CI derives it from /VERSION, e.g. 3.2.8+git).
FROM ubuntu:24.04
WORKDIR /tmp
ARG out_dir=/etc/freeradius
ARG PKG_VERSION=3.2.8+git
# Gitea Debian package registry (override for a different instance/owner).
ARG DEB_REGISTRY=https://git.ahax86.ru/api/packages/pub/debian
ARG DEB_DISTRIBUTION=noble
ARG DEB_COMPONENT=main
COPY packaging/image/local-noble.list /etc/apt/sources.list.d/ubuntu.sources
RUN apt-get update && apt-get install -y apt-transport-https ca-certificates curl gpg
# Register the Gitea Debian registry (public read assumed for owner "pub").
RUN install -d /etc/apt/keyrings && \
curl -fsSL "${DEB_REGISTRY}/repository.key" -o /etc/apt/keyrings/gitea-pub.asc && \
echo "deb [signed-by=/etc/apt/keyrings/gitea-pub.asc] ${DEB_REGISTRY} ${DEB_DISTRIBUTION} ${DEB_COMPONENT}" \
> /etc/apt/sources.list.d/gitea-freeradius.list
RUN apt-get update && apt-get install -y \
libfreeradius3=${PKG_VERSION} freeradius-common=${PKG_VERSION} \
freeradius-postgresql=${PKG_VERSION} freeradius-rest=${PKG_VERSION} \
freeradius-utils=${PKG_VERSION} freeradius=${PKG_VERSION} && \
apt autoremove --purge -y && apt-get clean && \
rm -f /etc/apt/sources.list.d/gitea-freeradius.list
# Bake the shared configs (kept under packaging/hosted/config).
COPY packaging/hosted/config /tmp/config
RUN cp /tmp/config/radiusd.conf $out_dir/radiusd.conf && \
cp /tmp/config/dictionary $out_dir/dictionary && \
cp /tmp/config/clients.conf $out_dir/clients.conf && \
cp /tmp/config/mods-enabled/raw $out_dir/mods-enabled/raw && \
cp /tmp/config/sites-enabled/dynamic-clients $out_dir/sites-enabled/dynamic-clients && \
rm -f $out_dir/sites-enabled/inner-tunnel && \
rm -f $out_dir/mods-enabled/eap && \
rm -f $out_dir/mods-enabled/proxy_rate_limit
# Enable delay_reject.
RUN rm -f $out_dir/mods-enabled/delay && \
ln -s $out_dir/mods-available/delay $out_dir/mods-enabled/delay
ADD packaging/image/entrypoint.sh /usr/bin/entrypoint.sh
RUN chmod a+x /usr/bin/entrypoint.sh && chmod -R 750 $out_dir && chown -R freerad:root $out_dir && \
mv /etc/freeradius/certs /tmp && \
rm -rf /tmp/config
WORKDIR /etc/freeradius
ENTRYPOINT ["/usr/bin/entrypoint.sh"]
CMD ["freeradius", "-f"]