Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

FreeTAKServer on Kubernetes

Production-grade Kubernetes deployment of FreeTAKServer on k3s — hardened Docker image, GitLab CI/CD with Kaniko, Traefik ingress with automatic TLS and per-IP rate limiting.

FreeTAKServer (FTS) is an open-source Python implementation of a TAK server, enabling ATAK/WinTAK clients to share geospatial information, chat, exchange files, and more. This repository contains everything needed to build and deploy it as a production Kubernetes workload.


Architecture

graph TD
    ATAK["ATAK / WinTAK Client"]
    LB["LoadBalancer Service\n(TAK ports: 8080, 8089, 8443)"]
    HTTP["Browser / API Consumer"]
    TRAEFIK["Traefik Ingress\n(HTTPS only)"]
    MW["Middleware\nRate limit: 20 req/s burst 40 per IP"]
    API_SVC["ClusterIP Service\n(:19023 REST API)"]
    POD["FreeTAKServer Pod\n(non-root user freetak)"]
    INIT["initContainer: fix-permissions\n(busybox, runAsUser: 0, chown 999+chmod 770)"]
    PVC["PersistentVolumeClaim\nSQLite DB · TLS certs · Data packages"]
    SECRET["k8s Secret\nKeys · Passwords · Addresses"]
    CERTMGR["cert-manager\n(Let's Encrypt)"]
    GITLAB["GitLab CI\nKaniko build → kubectl deploy"]

    ATAK -->|"SSL CoT :8089\nData packages :8080/:8443"| LB
    HTTP -->|HTTPS :443| TRAEFIK
    TRAEFIK --> MW --> API_SVC --> POD
    LB --> POD
    INIT -->|"pre-creates dirs + DB file\nchmod 777 before app starts"| PVC
    POD --> PVC
    POD -.->|"envFrom secretRef"| SECRET
    CERTMGR -.->|"TLS certificate"| TRAEFIK
    GITLAB -.->|"image push + rollout"| POD
Loading

What this project solves

FTS was not designed to run in Kubernetes out of the box. Getting it there required identifying and fixing several non-obvious issues:

1. Non-root container with named user

The Dockerfile creates a dedicated system user freetak and switches to it before the app starts (USER freetak). This prevents privilege escalation if the container is ever compromised.

The catch: Kubernetes runAsNonRoot: true requires a numeric UID to verify the constraint at runtime. Because the image uses a named user rather than a UID, runAsNonRoot cannot be set — k8s would reject the pod at admission. The workaround is to let the Dockerfile enforce non-root via USER, and enable seccompProfile: RuntimeDefault at the pod level to still reduce the syscall attack surface.

2. PVC permission bootstrap via initContainer

local-path volumes are provisioned root-owned. The freetak user (UID 999) cannot write to them without an explicit permission step.

Additionally, FTS calls validate_and_sanitize_path() on FTS_DB_PATH and FTS_LOGFILE_PATH before SQLAlchemy has a chance to create the DB file. If those paths don't exist at startup, FTS aborts.

An initContainer (busybox, runAsUser: 0) pre-creates the required directory tree, touches FTSDataBase.db, sets ownership to freetak (UID 999) via chown -R 999:999, and applies chmod 770 on the volume before the main container starts.

/opt/fts/
├── FTSDataBase.db                   # touched by initContainer
├── Logs/                            # created by initContainer
├── certs/                           # created by initContainer
├── ExCheck/
│   ├── template/
│   └── checklist/
└── FreeTAKServerDataPackageFolder/

3. Upstream dependency conflict: opentelemetry-sdk ≥ 1.24.0

opentelemetry-sdk >= 1.24.0 made BatchSpanProcessor.span_exporter a read-only property. This silently broke digitalpy == 0.3.13.7 (the version shipped with FTS v2.2.1) at runtime with an AttributeError.

The fix is a pinned force-reinstall after the editable FTS install so pip cannot upgrade it as a transitive dependency:

RUN pip install --force-reinstall "opentelemetry-sdk<1.24.0"

4. SQLite + Recreate strategy

FTS uses a SQLite database stored on the PVC. SQLite does not support concurrent writes from multiple processes. The Deployment uses strategy: Recreate to guarantee that only one pod holds the DB file at a time — no split-brain, no file corruption on rollout.

5. Secret rotation detection

The CI pipeline captures the Secret's resourceVersion before and after applying it. If the version changed (credentials were rotated), it automatically triggers a kubectl rollout restart so the pod picks up the new values. Kubernetes does not hot-reload envFrom: secretRef values.

6. Long-lived registry pull secret

CI_JOB_TOKEN / CI_REGISTRY_USER are ephemeral and expire when the CI job ends. The k8s node needs to pull the image independently (e.g. after a node reboot). A GitLab Deploy Token with read_registry scope is stored as a long-lived docker-registry secret (gitlab-registry-secret) and referenced in imagePullSecrets.

7. Traefik rate limiting before the Ingress

The Middleware CRD (k8s/middleware.yml) must exist before the Ingress is applied, otherwise Traefik cannot resolve the router.middlewares annotation and will silently drop the rule. The deploy job applies middleware.yml first, then ingress.yml.


Stack

Layer Technology
Runtime k3s (Kubernetes)
Ingress Traefik v2
TLS cert-manager + Let's Encrypt
Container build Kaniko (no privileged mode)
CI/CD GitLab CI
Persistence local-path PVC (SQLite)
Secrets k8s Secret + GitLab masked variables
Base image python:3.11-slim

Repository structure

.
├── Dockerfile                  # Hardened FTS image (non-root user, pinned deps)
├── .gitlab-ci.yml              # Build (Kaniko) + deploy (kubectl) pipeline
├── .gitmodules                 # freetakserver-src → FreeTAKTeam/FreeTakServer
└── k8s/
    ├── namespace.yml           # freetakserver namespace
    ├── pvc.yml                 # 5 Gi local-path volume for DB, certs, data packages
    ├── deployment.yml          # Pod spec: initContainer, envFrom secret, probes
    ├── service.yml             # LoadBalancer (TAK ports) + ClusterIP (REST API)
    ├── ingress.yml             # Traefik HTTPS ingress for the REST API
    ├── middleware.yml          # Per-IP rate limiting (20 req/s, burst 40)
    └── secret.example.yml      # Secret schema reference (do not apply directly)

Getting started

This repository uses a git submodule for the FTS application source. Clone with:

git clone --recurse-submodules https://github.com/YOUR_USERNAME/freetakserver-k8s.git

Or, if you already cloned without --recurse-submodules:

git submodule update --init --recursive

Note: The CI/CD pipeline (.gitlab-ci.yml) is written for GitLab CI. If you want to adapt it for GitHub Actions, the Kaniko build step and the kubectl deploy step map directly — the main difference is variable syntax (${{ secrets.X }} vs $X) and the runner tag.

For the full deployment flow, see CI/CD variables and Deployment flow below.


CI/CD variables

Set these in GitLab → Settings → CI/CD → Variables:

Variable Description Masked
KUBECONFIG_CONTENT kubeconfig file for kubectl
REGISTRY_DEPLOY_USER GitLab Deploy Token username (read_registry scope)
REGISTRY_DEPLOY_TOKEN Deploy Token password
FTS_DP_ADDRESS Public IP / hostname for DataPackage connections
FTS_USER_ADDRESS Public IP / hostname for CoT connections
FTS_SECRET_KEY FTS internal secret (≥ 16 random chars)
FTS_FED_PASSWORD Federation service password
FTS_CLIENT_CERT_PASSWORD SSL client certificate password
FTS_NODE_ID Unique node identifier (alphanumeric, ≤ 32 chars)
FTS_CONNECTION_MESSAGE (optional) Welcome message for ATAK clients
FTS_API_DOMAIN (optional) Domain for the REST API Ingress

Deployment flow

git push → main
    │
    ├─ build:freetakserver
    │   └─ Kaniko builds image from Dockerfile + freetakserver-src/ submodule
    │      Tags: :latest and :<commit-sha>
    │      Pushes to GitLab Container Registry
    │
    └─ deploy:freetakserver
        ├─ kubectl apply namespace, pull secret, app secret, PVC
        ├─ sed replaces __FTS_IMAGE__ placeholder → kubectl apply deployment
        ├─ kubectl apply services
        ├─ kubectl rollout status (timeout: 5 min)
        ├─ if secret changed → kubectl rollout restart
        ├─ kubectl apply middleware
        └─ if FTS_API_DOMAIN set → sed replaces __FTS_API_DOMAIN__ → kubectl apply ingress

TAK ports reference

Port Protocol Purpose
8080 TCP HTTP DataPackage service
8087 TCP Plain CoT (internal only)
8089 TCP (TLS) SSL Cursor-on-Target
8443 TCP (TLS) HTTPS DataPackage service
9000 TCP FTS Federation
19023 TCP REST API (Ingress only)

Acknowledgements

FreeTAKServer is developed and maintained by the FreeTAKTeam and released under the Eclipse Public License 2.0. This repository contains only infrastructure and deployment code — not the FTS application source.

About

Production-grade Kubernetes deployment of FreeTAKServer on k3s — hardened non-root Docker image, Kaniko build, GitLab CI/CD with commit-pinned images, Traefik ingress with automatic TLS and per-IP rate limiting.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages