Skip to main content

Sincronizzatore team tra authentik e grafana

Sincronizzazione automatica dei gruppi authentik verso Grafana OSS

Repo

https://gitlab.eagleprojects.cloud/devsecops/sync_team_from_authentik_to_grafana

Scopo

Questo servizio sincronizza automaticamente i gruppi presenti in authentik con i Team di Grafana OSS.

La regola di sincronizzazione è diretta:

Gruppo authentik  →  Team Grafana con lo stesso nome

Per ogni gruppo authentik:

  • viene creato il Team Grafana corrispondente, se non esiste;
  • vengono aggiunti al Team gli utenti appartenenti al gruppo;
  • vengono rimossi dal Team gli utenti che non appartengono più al gruppo;
  • gli utenti non ancora presenti in Grafana vengono ignorati fino al loro primo accesso;
  • i Team presenti solo in Grafana vengono eliminati esclusivamente quando DELETE_STALE_TEAMS=true.

La sincronizzazione è unidirezionale:

authentik  ────────────────>  Grafana
   fonte autorevole              destinazione

Le modifiche effettuate manualmente sui Team Grafana possono essere sovrascritte alla sincronizzazione successiva.


Architettura

+--------------------------+
|        authentik         |
|--------------------------|
| Utenti                   |
| Gruppi                   |
| Membership               |
| API token                |
+------------+-------------+
             |
             | HTTPS / REST API
             | Authorization: Bearer <token>
             v
+--------------------------+
| authentik-grafana-sync   |
|--------------------------|
| Container Python 3       |
| Lettura gruppi e utenti  |
| Confronto membership     |
| Creazione Team           |
| Aggiornamento membri     |
| Eventuale cancellazione  |
+------------+-------------+
             |
             | HTTPS / Grafana HTTP API
             | Authorization: Bearer <token>
             v
+--------------------------+
|       Grafana OSS        |
|--------------------------|
| Utenti OAuth esistenti   |
| Team                     |
| Membership dei Team      |
| Service Account token    |
+--------------------------+

Flusso di autenticazione degli utenti

La sincronizzazione dei Team è separata dall'autenticazione interattiva degli utenti.

Utente
  |
  | 1. Accesso a Grafana
  v
Grafana
  |
  | 2. Redirect OIDC/OAuth
  v
authentik
  |
  | 3. Autenticazione completata
  v
Grafana
  |
  | 4. Creazione/aggiornamento utente Grafana
  v
Utente disponibile per la sincronizzazione dei Team

Lo script non crea utenti Grafana.

Un utente authentik può essere aggiunto a un Team soltanto dopo avere effettuato almeno un accesso a Grafana tramite authentik.


Flusso di sincronizzazione

+----------------------------------------------------+
| 1. Lettura di tutti i gruppi da authentik          |
+------------------------------+---------------------+
                               |
                               v
+----------------------------------------------------+
| 2. Lettura di tutti gli utenti da authentik        |
+------------------------------+---------------------+
                               |
                               v
+----------------------------------------------------+
| 3. Lettura organizzazione, utenti e Team Grafana   |
+------------------------------+---------------------+
                               |
                               v
+----------------------------------------------------+
| 4. Per ogni gruppo authentik                       |
|                                                    |
|    Il Team con lo stesso nome esiste?              |
|                                                    |
|        no  ──> creazione Team                      |
|        sì  ──> utilizzo Team esistente             |
+------------------------------+---------------------+
                               |
                               v
+----------------------------------------------------+
| 5. Confronto membership                            |
|                                                    |
|    utenti desiderati - utenti presenti = aggiunte  |
|    utenti presenti - utenti desiderati = rimozioni |
+------------------------------+---------------------+
                               |
                               v
+----------------------------------------------------+
| 6. DELETE_STALE_TEAMS=true?                        |
|                                                    |
|        no  ──> conserva Team solo Grafana          |
|        sì  ──> elimina Team assenti in authentik   |
+------------------------------+---------------------+
                               |
                               v
+----------------------------------------------------+
| 7. Scrittura riepilogo nei log                     |
+----------------------------------------------------+

Regole di sincronizzazione

Nomi dei gruppi

Il nome del Team Grafana coincide con il nome del gruppo authentik.

authentik                  Grafana
------------------------------------------------
Administrators      →      Administrators
Developers          →      Developers
Monitoring          →      Monitoring
Network Operations  →      Network Operations

Non vengono applicati:

  • prefissi;
  • suffissi;
  • mapping;
  • filtri;
  • rinominazioni.

I nomi vengono confrontati senza distinguere maiuscole e minuscole. Due gruppi authentik i cui nomi differiscono soltanto per il case producono un errore, perché colliderebbero nello stesso Team Grafana.

Esempio non valido:

Developers
developers

Membership

La membership dei Team viene sincronizzata esattamente.

authentik group "Developers"
    ├── alice@example.com
    ├── bob@example.com
    └── carol@example.com

Grafana team "Developers"
    ├── alice@example.com
    ├── bob@example.com
    └── carol@example.com

Alla sincronizzazione successiva:

authentik group "Developers"
    ├── alice@example.com
    └── carol@example.com

lo script rimuove bob@example.com dal Team Grafana.

Corrispondenza degli utenti

Lo script cerca l'utente Grafana usando:

  1. indirizzo e-mail;
  2. username/login come fallback.
Utente authentik
     |
     +--> e-mail presente in Grafana? ---- sì ---> utente trovato
     |
     +--> no
            |
            +--> username presente? ------ sì ---> utente trovato
            |
            +--> no -----------------------------> utente ignorato

Se l'utente non è ancora presente in Grafana, viene prodotto un warning:

User not yet present in Grafana; skipping for team 'Developers'

L'utente verrà incluso automaticamente dopo:

  1. il primo accesso a Grafana tramite authentik;
  2. la successiva esecuzione dello script.

Utenti esclusi

Non vengono sincronizzati:

  • utenti authentik disabilitati;
  • service account authentik.

Gruppi annidati

La versione corrente calcola la membership effettiva dei gruppi includendo anche gli utenti appartenenti ai gruppi discendenti.

Esempio:

Engineering
├── Backend
│   ├── Alice
│   └── Bob
└── Frontend
    ├── Carol
    └── David

Risultato:

Team Grafana "Engineering"
├── Alice
├── Bob
├── Carol
└── David

I Team Backend e Frontend vengono comunque creati e sincronizzati separatamente.


Requisiti

Infrastruttura

  • Docker Engine;
  • Docker Compose v2;
  • connettività HTTPS dal container verso authentik;
  • connettività HTTPS dal container verso Grafana;
  • risoluzione DNS funzionante;
  • orario del sistema sincronizzato.

authentik

Sono necessari:

  • URL dell'istanza authentik;
  • API token;
  • permessi di lettura su utenti e gruppi.

Endpoint utilizzati:

GET /api/v3/core/groups/
GET /api/v3/core/users/

Grafana

Sono necessari:

  • Grafana OSS;
  • autenticazione authentik già funzionante tramite Generic OAuth/OIDC;
  • Service Account Grafana;
  • Service Account token con ruolo Admin nell'organizzazione interessata.

Endpoint principali utilizzati:

GET    /api/org/
GET    /api/org/users
GET    /api/teams/search
POST   /api/teams
GET    /api/teams/{teamId}/members
POST   /api/teams/{teamId}/members
DELETE /api/teams/{teamId}/members/{userId}
DELETE /api/teams/{teamId}

Struttura del progetto

authentik-grafana-sync/
├── authentik_grafana_sync.py
├── Dockerfile
├── compose.yaml
├── docker-entrypoint.sh
├── requirements.txt
├── .env.example
├── .dockerignore
└── README.md

Configurazione

Creare il file .env:

cp .env.example .env
chmod 600 .env

Configurazione di esempio:

# authentik
AUTHENTIK_URL=https://authentik.example.com
AUTHENTIK_TOKEN=replace-with-authentik-api-token
AUTHENTIK_VERIFY_TLS=true

# Grafana
GRAFANA_URL=https://grafana.example.com
GRAFANA_TOKEN=replace-with-grafana-service-account-token
GRAFANA_VERIFY_TLS=true

# Opzionale: organizzazione Grafana attesa
GRAFANA_ORG_ID=1

# Conserva o elimina i Team presenti solo in Grafana
DELETE_STALE_TEAMS=false

# Timeout HTTP
HTTP_TIMEOUT_SECONDS=30

# Modalità simulazione
DRY_RUN=false

# Esecuzione container
SYNC_INTERVAL_SECONDS=300
RUN_ONCE=false

Variabili di ambiente

VariabileObbligatoriaDefaultDescrizione
AUTHENTIK_URLURL base di authentik
AUTHENTIK_TOKENAPI token authentik
AUTHENTIK_VERIFY_TLSnotrueVerifica il certificato TLS di authentik
GRAFANA_URLURL base di Grafana
GRAFANA_TOKENService Account token Grafana
GRAFANA_VERIFY_TLSnotrueVerifica il certificato TLS di Grafana
GRAFANA_ORG_IDnoVerifica che il token operi sull'organizzazione prevista
DELETE_STALE_TEAMSnofalseElimina i Team Grafana assenti in authentik
HTTP_TIMEOUT_SECONDSno30Timeout delle richieste HTTP
DRY_RUNnofalseSimula le modifiche senza scrivere in Grafana
SYNC_INTERVAL_SECONDSno300Intervallo tra due sincronizzazioni
RUN_ONCEnofalseEsegue una sola sincronizzazione e termina

Creazione del token authentik

Creare un service account o un utente tecnico in authentik con privilegi di lettura su utenti e gruppi.

Generare un API token e inserirlo nel file .env:

AUTHENTIK_TOKEN=ak_xxxxxxxxxxxxxxxxx

Il valore deve contenere soltanto il token, senza il prefisso Bearer.

Corretto:

AUTHENTIK_TOKEN=ak_xxxxxxxxxxxxxxxxx

Errato:

AUTHENTIK_TOKEN=Bearer ak_xxxxxxxxxxxxxxxxx

Creazione del Service Account Grafana

In Grafana:

Administration
└── Users and access
    └── Service accounts

Creare un Service Account, ad esempio:

authentik-grafana-sync

Assegnare il ruolo:

Admin

Generare un token e salvarlo nel file .env:

GRAFANA_TOKEN=glsa_xxxxxxxxxxxxxxxxx

Anche in questo caso il valore deve contenere soltanto il token.


Deployment con Docker Compose

Build

docker compose build

Avvio

docker compose up -d

Verifica dello stato

docker compose ps

Visualizzazione dei log

docker compose logs -f sync

Arresto

docker compose down

Aggiornamento dello script

Quando viene modificato authentik_grafana_sync.py, occorre ricostruire l'immagine:

docker compose down
docker compose build --no-cache sync
docker compose up -d --force-recreate

Modificare soltanto .env non richiede una nuova build, ma richiede la ricreazione del container:

docker compose up -d --force-recreate

Prima esecuzione

Eseguire sempre inizialmente un dry-run.

docker compose run --rm \
  -e RUN_ONCE=true \
  -e DRY_RUN=true \
  sync

Il dry-run:

  • legge authentik;
  • legge Grafana;
  • calcola le modifiche;
  • mostra le operazioni nei log;
  • non crea Team;
  • non aggiunge o rimuove utenti;
  • non elimina Team.

Esempio:

[dry-run] Would create Grafana team 'Developers'
[dry-run] Would add Grafana user ID 42 to team 'Developers'
[dry-run] Would remove old.user from team 'Monitoring'

Dopo avere verificato il risultato:

docker compose run --rm \
  -e RUN_ONCE=true \
  -e DRY_RUN=false \
  sync

Esecuzione periodica

Il container esegue immediatamente una sincronizzazione e poi attende il numero di secondi configurato.

Avvio container
      |
      v
Sincronizzazione
      |
      v
Attesa SYNC_INTERVAL_SECONDS
      |
      v
Sincronizzazione
      |
      v
      ...

Intervallo predefinito:

SYNC_INTERVAL_SECONDS=300

Corrisponde a una sincronizzazione ogni cinque minuti.

Esempio ogni quindici minuti:

SYNC_INTERVAL_SECONDS=900

Applicare la modifica:

docker compose up -d --force-recreate

Cancellazione dei Team obsoleti

Modalità conservativa

DELETE_STALE_TEAMS=false
Gruppo authentik: assente
Team Grafana:     presente
Risultato:        Team conservato

Questa è la configurazione consigliata durante l'installazione iniziale.

Modalità autoritativa

DELETE_STALE_TEAMS=true
Gruppo authentik: assente
Team Grafana:     presente
Risultato:        Team eliminato

Schema decisionale:

Team Grafana
    |
    v
Esiste un gruppo authentik con lo stesso nome?
    |
    +---- sì ----> conserva e sincronizza
    |
    +---- no ----> DELETE_STALE_TEAMS?
                       |
                       +---- false ----> conserva
                       |
                       +---- true -----> elimina

Attenzione: con DELETE_STALE_TEAMS=true, authentik diventa la fonte autoritativa per tutti i Team dell'organizzazione Grafana. Anche i Team creati manualmente in Grafana vengono eliminati se non esiste un gruppo authentik con lo stesso nome.


Sicurezza del container

Il container è configurato con:

  • utente non root;
  • filesystem in sola lettura;
  • /tmp montato come tmpfs;
  • rimozione di tutte le Linux capabilities;
  • no-new-privileges;
  • token forniti tramite variabili di ambiente;
  • nessun token salvato nell'immagine.

Schema:

Container
├── USER non-root
├── read_only: true
├── cap_drop: ALL
├── no-new-privileges
└── /tmp in RAM

Proteggere il file .env:

chmod 600 .env

Non includere .env nel repository Git.


Log operativi

Avvio normale

Starting periodic synchronization every 300 seconds.
2026-07-20T12:57:53Z Starting synchronization.
INFO Reading authentik groups and users
INFO Loaded 8 authentik groups and 501 users
INFO Using Grafana organization 'Main Org.' (ID 1)

Riepilogo finale

Synchronization completed:
teams_created=2
teams_deleted=0
members_added=18
members_removed=3
users_missing=4
ContatoreDescrizione
teams_createdTeam Grafana creati
teams_deletedTeam Grafana eliminati
members_addedUtenti aggiunti ai Team
members_removedUtenti rimossi dai Team
users_missingUtenti authentik non ancora presenti in Grafana

Troubleshooting

401 Unauthorized da Grafana

Errore:

GET https://grafana.example.com/api/org/ returned HTTP 401

Possibili cause:

  • token Grafana errato;
  • token scaduto o eliminato;
  • valore configurato come Bearer glsa_...;
  • container non ricreato dopo la modifica di .env;
  • reverse proxy che rimuove l'header Authorization;
  • token appartenente a un'altra istanza Grafana.

Verificare il token dal container:

docker compose exec -T sync python3 - <<'PY'
import os
import requests

url = os.environ["GRAFANA_URL"].rstrip("/")
token = os.environ["GRAFANA_TOKEN"].strip()

response = requests.get(
    url + "/api/org/",
    headers={"Authorization": "Bearer " + token},
    timeout=30,
)

print("Status:", response.status_code)
print("Body:", response.text)
PY

Risultato atteso:

Status: 200
Body: {"id":1,"name":"Main Org."}

403 Forbidden da Grafana

Il token è valido ma il Service Account non dispone dei permessi necessari.

Assegnare al Service Account il ruolo organizzativo:

Admin

Utente ignorato perché non presente in Grafana

Warning:

User not yet present in Grafana; skipping for team 'Developers'

Procedura:

  1. fare accedere l'utente a Grafana tramite authentik;
  2. attendere la sincronizzazione successiva;
  3. verificare la membership del Team.

Il container continua a usare la vecchia versione

Verificare il file incluso nell'immagine:

docker compose exec sync \
  sha256sum /app/authentik_grafana_sync.py

Ricostruire senza cache:

docker compose down --remove-orphans
docker compose build --no-cache sync
docker compose up -d --force-recreate

Il dry-run rimane attivo

Controllare:

DRY_RUN=false

Verificare le variabili caricate:

docker compose exec sync env | grep DRY_RUN

Ricreare il container:

docker compose up -d --force-recreate

Certificato TLS non valido

Errore tipico:

certificate verify failed

Correggere preferibilmente la catena del certificato.

Solo per test temporanei:

AUTHENTIK_VERIFY_TLS=false
GRAFANA_VERIFY_TLS=false

Questa configurazione riduce la sicurezza e non è consigliata in produzione.

Grafana dietro reverse proxy

Il proxy deve inoltrare l'header:

Authorization: Bearer <token>

Testare, quando possibile, l'accesso diretto a Grafana:

GRAFANA_URL=http://grafana:3000

oppure:

GRAFANA_URL=http://host.docker.internal:3000

Se l'accesso diretto funziona e quello tramite proxy restituisce 401, il problema è nella configurazione del reverse proxy.

Organizzazione Grafana errata

Errore:

Grafana API context returned org ID 2, expected 1

Correggere:

GRAFANA_ORG_ID=2

oppure rimuovere GRAFANA_ORG_ID se non serve una verifica esplicita.


Verifiche operative

Verifica quotidiana

docker compose ps
docker compose logs --since=24h sync

Controllare che siano presenti messaggi:

Synchronization completed

e che non siano presenti errori HTTP ripetuti.

Verifica manuale di una membership

  1. aggiungere un utente a un gruppo authentik;
  2. attendere il successivo ciclo;
  3. aprire Grafana;
  4. verificare il Team con lo stesso nome;
  5. controllare la presenza dell'utente.

Verifica della rimozione

  1. rimuovere l'utente dal gruppo authentik;
  2. attendere il successivo ciclo;
  3. verificare che sia stato rimosso dal Team Grafana.

Verifica della creazione Team

  1. creare un nuovo gruppo authentik;
  2. attendere il successivo ciclo;
  3. verificare la creazione del Team omonimo in Grafana.

Verifica della cancellazione Team

Da eseguire soltanto con:

DELETE_STALE_TEAMS=true
  1. creare un Team di test in Grafana;
  2. non creare il gruppo omonimo in authentik;
  3. eseguire un dry-run;
  4. verificare il messaggio:
[dry-run] Would delete Grafana team 'Team di test'

Aggiornamento e rollback

Aggiornamento

docker compose down
docker compose build --no-cache sync
docker compose up -d --force-recreate
docker compose logs -f sync

Rollback

Conservare una copia della versione precedente:

authentik-grafana-sync/
├── releases/
│   ├── 2.0.0/
│   └── 2.0.1/
└── current/

Per il rollback:

cd releases/2.0.0
docker compose build --no-cache sync
docker compose up -d --force-recreate

Prima di ogni aggiornamento è consigliato eseguire la nuova immagine in dry-run.


Limiti noti

  • La sincronizzazione è associata a una singola organizzazione Grafana.
  • Gli utenti Grafana non vengono creati dallo script.
  • Un utente deve avere effettuato almeno un accesso OAuth a Grafana.
  • I Team Grafana non hanno un identificatore esterno authentik: la corrispondenza avviene tramite il nome.
  • Rinominare un gruppo authentik equivale a creare un nuovo Team.
  • Con DELETE_STALE_TEAMS=true, il Team con il vecchio nome viene eliminato.
  • I ruoli Grafana Viewer, Editor e Admin non vengono gestiti da questo script.
  • La sincronizzazione riguarda esclusivamente Team e membership.
  • La modifica manuale delle membership in Grafana viene sovrascritta.

Esempio completo

Stato iniziale authentik

Groups
├── Administrators
│   └── alice@example.com
├── Developers
│   ├── bob@example.com
│   └── carol@example.com
└── Monitoring
    └── david@example.com

Stato iniziale Grafana

Teams
├── Developers
│   └── old.user@example.com
└── Legacy Team

Sincronizzazione con DELETE_STALE_TEAMS=false

Teams
├── Administrators
│   └── alice@example.com
├── Developers
│   ├── bob@example.com
│   └── carol@example.com
├── Monitoring
│   └── david@example.com
└── Legacy Team

Sincronizzazione con DELETE_STALE_TEAMS=true

Teams
├── Administrators
│   └── alice@example.com
├── Developers
│   ├── bob@example.com
│   └── carol@example.com
└── Monitoring
    └── david@example.com

Comandi rapidi

# Build
docker compose build

# Avvio
docker compose up -d

# Log
docker compose logs -f sync

# Dry-run singolo
docker compose run --rm \
  -e RUN_ONCE=true \
  -e DRY_RUN=true \
  sync

# Esecuzione singola reale
docker compose run --rm \
  -e RUN_ONCE=true \
  -e DRY_RUN=false \
  sync

# Ricreazione dopo modifica .env
docker compose up -d --force-recreate

# Rebuild completo dopo modifica dello script
docker compose build --no-cache sync
docker compose up -d --force-recreate

# Arresto
docker compose down