Sari la conținut

GSSO — Technical Documentation

Acest conținut nu este încă disponibil în limba selectată.

Document codeSPEC-GSSO-2026 / Report 04
Version0.1-draft (EN source, governing)
Date2026-10-05
StatusDraft: describes the planned system. No code exists yet
Companion reports00 RFP · 01 ADR · 02 Requirements · 03 Consumers & contract · 05 Roadmap
VersionDateChanges
0.1-draft2026-10-05C4 views, components, data model, flows, realm baseline, security, deployment, takeover runbook, sizing, observability, testing
flowchart TB
admin([IAM admin / platform owner / approver / auditor / helpdesk])
staff([Staff user])
citizen([Citizen])
apps[gStack SaaS & PaaS<br/>interdictii, cancelaria, gdocs, gregistry, drumuri,<br/>CRM, gDocFlow, gTenders, glog, gnotify, gstorage, gflow]
mob[gsso_mob GovSign]
gsso[[GSSO<br/>identity & SSO platform]]
mpass[MPass / eID]
ldap[AD / LDAP of institutions]
glog[GLog]
gnotify[GNotify]
smtp[SMTP relay]
admin -- console --> gsso
staff -- login / SSO --> gsso
citizen -- login --> gsso
mob -- OIDC PKCE --> gsso
apps -- OIDC, JWKS, api/v1/app, Kafka events --> gsso
gsso -- SAML broker --> mpass
gsso -- LDAP federation --> ldap
gsso -- audit events --> glog
gsso -- alerts, expiry notices --> gnotify
gsso -- invitations, reset mails --> smtp
flowchart LR
subgraph edge[Edge]
nginx[nginx / ingress<br/>sso.gstack… · gsso.gstack…]
end
subgraph gssoSys[GSSO]
web[gsso-web<br/>Angular + GDS<br/>static]
gw[gsso-gateway<br/>JHipster 9.1 gateway · BFF]
srv[gsso<br/>JHipster 9.1 microservice]
db[(PostgreSQL<br/>gsso)]
kc[Keycloak 26.6.x<br/>+ gsso-kc-extensions<br/>+ theme gstack]
kdb[(PostgreSQL<br/>keycloak)]
boot[gsso-bootstrap<br/>keycloak-config-cli job]
end
kafka[[Kafka]]
glog[GLog]
gn[GNotify]
nginx --> web
nginx --> gw
nginx --> kc
gw -- /api/v1/** --> srv
gw -. OIDC code flow, client gsso-console .-> kc
srv -- Admin REST, SA gsso-reconciler --> kc
srv --- db
kc --- kdb
boot -- baseline realms --> kc
kc -- gsso.kc-events.v1 --> kafka
kafka --> srv
srv -- gsso.access-revoked.v1, gsso.grant.v1 --> kafka
srv -- audit --> glog
srv -- notify --> gn
ContainerTechResponsibility
gsso-webAngular (version per JHipster 9.1.0), @gstack/gds-angular, ngx-translate ro/ru/enConsole UI; no tokens (BFF cookie)
gsso-gatewayJHipster 9.1.0 gateway (Spring Cloud Gateway), OAuth2 clientLogin and session (BFF), CSRF, routing of /api/v1/**, rate limits on app zone
gssoJHipster 9.1.0 microservice, Spring Boot, Hibernate, Liquibase, Kafka, keycloak-admin-client 26.xCatalog, grants, reconciler, event consumer, relay to GLog, reports, admin and app APIs
Keycloakquay.io/keycloak/keycloak:26.6.x (custom image with extensions + theme, kc.sh build)Authentication, tokens, sessions, MFA, federation, account console
gsso-bootstrapadorsys/keycloak-config-cli matching 26.xApplies realm baselines on deploy (idempotent)
PostgreSQL ×2as generated by JHipster 9.1.0 (gsso); Keycloak-supported version (keycloak)Separate databases, backups, lifecycles
Kafkashared gStack cluster (Strimzi target; single broker in compose)Event transport

2.1 Repository layout (govtech/gstack/gsso)

Section titled “2.1 Repository layout (govtech/gstack/gsso)”
gsso/
CLAUDE.md gsso.jdl gsso.dc.html test-scenarios.md .env.example
docs/reports/ 00..05 (en, ro, ru), README, docx/, pdf/, img/
keycloak/
extensions/ Maven module: gsso-kafka event listener, mappers (roles, roles_scoped, org_unit, tenant), conditional-role authenticator
themes/gstack/ login/, account/, email/ (FreeMarker + GDS CSS, messages_ro/ru/en)
realms/ gstack.json, cetatean.json, tenant-template.json (keycloak-config-cli, ${ENV} placeholders)
Dockerfile FROM keycloak:26.6.x → kc.sh build with providers + theme
gsso/ JHipster 9.1.0 microservice (generated from gsso.jdl + custom code)
gsso-gateway/ JHipster 9.1.0 gateway
gsso-web/ Angular SPA on GDS
sdk/
gsso-spring-boot-starter/
gsso-angular/
deploy/
docker-compose.yml local + demo host stack
docker-compose.test.yml postgres + kafka + keycloak for IT (always `down -v`)
nginx/gsso.conf vhost templates (applied only with owner approval)
helm/gsso/ Kubernetes target
testing/results/
Package (systems.esempla.gsso)ComponentNotes
domain, repository, service, web.restJHipster-generated CRUD for JDL entitiesregenerated; custom code lives in separately named classes
keycloakKeycloakAdminGatewayOnly class touching keycloak-admin-client. Natural-key lookups, gsso.managed marker, error translation to RFC 7807
syncJobOutbox, ReconcilerWorker, RealmDiffer, DriftPolicyOutbox jobs, worker with FOR UPDATE SKIP LOCKED + per-realm advisory lock, periodic full diff
sync.handlersRealmHandler, ClientHandler, RoleHandler, RoleMappingHandler, UserHandler, PolicyHandler, OrgUnitHandlerOne handler per object type: apply(job), diff(realm)
grantGrantLifecycleService, GrantExpiryScheduler, ApprovalPolicyState machine, four-eyes, expiry, notifications
eventsKcEventConsumer, EventStore, HashChain, GlogRelay, RevocationPublisherIdempotent consumption, append-only store, forwarding, outgoing events
projectionUserProjectionUpdater, NightlyUserSyncProiectieUtilizator
reportDashboardAggregator, AccessReportKPIs, login buckets, auth mix
web.rest.admin, web.rest.appFacade controllersZones, scopes, realm scoping filter, Idempotency-Key store
securityRealmScopeAuthorizationManager, PlatformScopeAuthorizationManagerGSSO_REALM_ADMIN/GSSO_PLATFORM_OWNER scoping
notifyGnotifyClientExpiry warnings, alerts
erDiagram
REALM ||--o{ CLIENT_APLICATIE : contains
REALM ||--|| POLITICA_AUTENTIFICARE : has
REALM ||--o{ UNITATE_ORGANIZATIONALA : has
REALM ||--o{ PROIECTIE_UTILIZATOR : projects
PLATFORMA ||--o{ CLIENT_APLICATIE : owns
PLATFORMA ||--o{ ROL_PLATFORMA : exposes
PLATFORMA }o--o{ REALM : "deployed in"
ROL_PLATFORMA ||--o{ ROL_PLATFORMA : "composite of"
PACHET_ROLURI }o--o{ ROL_PLATFORMA : bundles
ATRIBUIRE_ACCES }o--|| ROL_PLATFORMA : grants
ATRIBUIRE_ACCES }o--o| PACHET_ROLURI : "via bundle"
ATRIBUIRE_ACCES }o--o| UNITATE_ORGANIZATIONALA : "scoped to"
ATRIBUIRE_ACCES ||--o{ APROBARE : "approved by"
UNITATE_ORGANIZATIONALA ||--o{ UNITATE_ORGANIZATIONALA : parent
JOB_SINCRONIZARE }o--|| REALM : targets
EVENIMENT_GSSO }o--|| REALM : in

The authoritative definition is gsso.jdl. Main fields:

EntityKey fieldsNotes
Realmnume (unique), denumireAfisata, tip {STAFF, CETATEAN, TENANT, SERVICE}, brokerAlias, politicaDrift {REPORT, ENFORCE, IGNORE}, stare {ACTIV, INACTIV}, stareSync, kcId, adoptat
Platformacod (unique), denumire, tip {SAAS, PAAS}, descriere (ro/ru/en), urlBaza, accent, stare, autoInregistrareowners via ProprietarPlatforma(sub)
ClientAplicatieclientId, protocol {OIDC, SAML}, tipAcces {PUBLIC_PKCE, CONFIDENTIAL, BEARER_ONLY, SERVICE_ACCOUNT}, redirectUris, webOrigins, postLogoutUris, backchannelLogoutUrl, audienta, scopuri, audienteSchimb, sensibil, stare, stareSync, kcIdunique (realm, clientId)
RolPlatformacod (prefixed, unique per realm), descriereRo/Ru/En, compozit, sensibil, valabilitateImplicitaZile, stareSync, kcIdchildren via self-relation
PachetRoluricod, denumire, rolesreconciled as Keycloak group
AtribuireAccesuserSub, realm, rol / pachet, unitate, validDe, validPana, stare {SOLICITATA, APROBATA, ACTIVA, RESPINSA, REVOCATA, EXPIRATA}, solicitant, motiv, sursa {CONSOLA, APP, ADOPTAT, DIRECT}, motivRevocare
Aprobareatribuire, aprobator, nivel (1/2), decizie, comentariu, data
UnitateOrganizationalacod, denumire (ro/ru/en), parinte, sefi
PoliticaAutentificareparola, otp, webauthn, qr, client, broker, magic (booleans), politicaParola, mfaRoluriSensibile, sesiuneInactivMin, sesiuneMaxOre, tokenAccesMinone per realm
JobSincronizarerealm, tipObiect, cheieObiect, operatie {CREATE, UPDATE, DELETE, MAP, UNMAP, DIFF}, stare {PENDING, RUNNING, OK, FAILED, DRIFT, ACCEPTAT}, incercari, urmatoareaIncercare, eroare, diff (jsonb), corelatie
ProiectieUtilizatorsub, realm, username, nume, email, idnpCriptat, activ, mfa (set), ultimaAutentificare, ultimulEsec, unitate, locale, actualizatLaread model
EvenimentGssoid (UUID), realm, tip, categorie {KC_USER, KC_ADMIN, CATALOG, GRANT, SYNC, CONSOLE}, actor, subiect, client, ip, moment, payload (jsonb), hashPrecedent, hash, glogIdappend-only; monthly partitions

JSONB columns (diff, payload, descriere) are declared as TextBlob in the JDL and converted by an extra Liquibase changelog with @JdbcTypeCode(SqlTypes.JSON). Changelogs are append-only.

5.1 Register a platform → clients and roles in Keycloak

Section titled “5.1 Register a platform → clients and roles in Keycloak”
sequenceDiagram
actor A as Platform admin
participant W as gsso-web
participant G as gsso-gateway
participant S as gsso
participant DB as gsso DB
participant R as ReconcilerWorker
participant K as Keycloak
A->>W: Create platform "crm" + clients + roles
W->>G: POST /api/v1/admin/platforme (Idempotency-Key)
G->>S: relay (user token)
S->>DB: tx: Platforma, ClientAplicatie*, RolPlatforma*, JobSincronizare*, EvenimentGsso
S-->>W: 201 (stareSync=PENDING)
R->>DB: take jobs (SKIP LOCKED, realm lock)
R->>K: GET client by clientId → absent → POST client (gsso.managed=true)
R->>K: POST roles, composites, audience mapper
R->>DB: job OK, kcId stored, stareSync=IN_SYNC, event SYNC_OK
W->>G: poll / refresh → badge "in sync"
sequenceDiagram
actor Req as Requester
actor Own as Platform owner
actor Ap2 as Approver (sensitive)
participant S as gsso
participant K as Keycloak
participant N as GNotify
Req->>S: POST /admin/atribuiri {sub, CRM_ADMIN, motiv}
S->>S: SOLICITATA (+event)
S->>N: notify owner
Own->>S: approve (level 1)
alt role sensitive
Ap2->>S: approve (level 2, ≠ requester, ≠ owner approval)
end
S->>S: APROBATA + Job MAP
S->>K: role-mapping add (reconciler)
K-->>S: 204
S->>S: ACTIVA (+event, gsso.grant.v1)
Note over K: next token of the user contains CRM_ADMIN in roles
sequenceDiagram
actor O as Owner
participant S as gsso
participant K as Keycloak
participant Kf as Kafka
participant App as App (starter)
O->>S: revoke grant (motiv)
S->>K: role-mapping delete
S->>K: POST /users/{id}/logout (sessions + back-channel logout)
S->>Kf: gsso.access-revoked.v1 {sub, notBefore=now}
Kf->>App: deny-list sub until token TTL
App-->>App: requests with iat < notBefore → 401
S->>S: REVOCATA (+event → GLog)
sequenceDiagram
participant U as User
participant K as Keycloak (+gsso-kafka)
participant Kf as Kafka
participant S as gsso
participant L as GLog
U->>K: login (password + OTP)
K-->>U: tokens
K--)Kf: LOGIN event (async)
Kf->>S: consume (dedup eventId)
S->>S: EvenimentGsso (hash chain), projection.lastLogin, aggregates
S->>L: POST /api/v1/app/audit-events (client credentials)
L-->>S: receipt id
  1. Every 15 min, or on demand, RealmDiffer loads the desired state for the realm and reads Keycloak (clients, roles, composites, mappings of managed roles, groups, flows and policy fields it manages).
  2. For each difference it creates a JobSincronizare(DIFF):
    • missing in Keycloak → CREATE (when the policy is ENFORCE) or DRIFT;
    • different → UPDATE or DRIFT;
    • present in Keycloak with gsso.managed=true but absent in GSSO → DELETE or DRIFT;
    • present without the marker → NEGUVERNAT (report only).
  3. In the console an admin can retry, enforce (apply the GSSO value) or accept drift (copy the Keycloak value into GSSO).

5.6 Adoption of an existing realm (e.g. interdictii)

Section titled “5.6 Adoption of an existing realm (e.g. interdictii)”

This is a dry run first (GSSO-FR-080), then the import:

  • clients are grouped into platforms by a mapping file (clientId → platform code);
  • realm roles are mapped to platform roles through rename rules (for example ROLE_ADMIN → per-platform <APP>_ADMIN candidates, flagged for review);
  • current user→role mappings become AtribuireAcces(ACTIVA, sursa=ADOPTAT).

Nothing is written to Keycloak while the drift policy is IGNORE.

  1. The browser opens gsso.gstack…, and gsso-gateway redirects it to Keycloak (realm gstack, client gsso-console, PKCE + confidential).
  2. The user logs in. MFA is mandatory for any GSSO_* role (NFR-SEC-008).
  3. The gateway stores the tokens in the server-side session and sets the SESSION cookie (HttpOnly, Secure, SameSite=Lax) plus the CSRF cookie.
  4. SPA calls carry the cookie. The gateway relays the access token to gsso, which applies the realm and platform scope from the GSSO_* roles and their attributes.
  1. A cetatean client redirects the citizen to Keycloak, which brokers to MPass (SAML POST).
  2. The assertion returns. The first-broker-login flow looks up the user by the IDNP attribute; it links the account if found, otherwise it creates the user with the mapped attributes.
  3. Keycloak sets the acr value from the MPass assurance level, and the token is issued for the portal.

6. Realm baseline (keycloak/realms/gstack.json, summary)

Section titled “6. Realm baseline (keycloak/realms/gstack.json, summary)”
SettingValue
Login theme / account / emailgstack (GDS), locales ro, ru, en, default ro
Brute forceon, 5 failures, wait increment 60 s, max 15 min
Password policylength(12) and upperCase(1) and lowerCase(1) and digits(1) and notUsername and notEmail and passwordHistory(5)
SSO session idle / max30 min / 10 h
Access token5 min
Eventsuser + admin events enabled, admin events with representation, listeners jboss-logging, gsso-kafka, expiry 7 days
Client scopes (default)profile, email, gsso-roles (flat roles), gsso-org (org_unit, org_unit_path, roles_scoped), gsso-locale, acr
Client scopes (optional)idnp, gsso-roles-filtered, offline_access
Browser flowCookie (alt) → Identity provider redirector (alt) → Forms: username+password (req) → Conditional OTP/WebAuthn (condition: user has role GSSO_MFA_REQUIRED or any role flagged sensitive)
Default rolesROLE_USER
Baseline clientsgsso-console (confidential, standard flow, PKCE), gsso-api (bearer), gsso-reconciler lives in master
Required actionsverify e-mail, update password, configure OTP, WebAuthn register, terms (disabled)
Token exchangestandard token exchange V2 enabled; per-client permissions from GSSO

cetatean.json uses the same theme and different settings:

  • an MPass SAML identity provider with a first-broker-login flow that matches on IDNP;
  • no LDAP;
  • password login disabled by default;
  • session idle 15 min.

tenant-template.json is a parameterised copy of gstack without federation.

ConcernMeasure
Console accessBFF; GSSO_* roles; MFA required; idle 15 min; CSRF; CSP
Realm scopingGSSO_REALM_ADMIN carries attribute gsso.realm; server filter on every query and command; tested by authorization matrix
Platform scopingGSSO_PLATFORM_OWNER with attribute gsso.platforma (multi-valued)
ReconcilerService account in master with only manage-*/view-* roles of managed realms (fine-grained admin permissions V2); credentials in secret store; rotated every 90 days
Four-eyesApprovalPolicy: requester ≠ approver; for sensitive roles two distinct approvers; GSSO roles themselves are sensitive
SecretsClient secrets are generated by Keycloak, shown once in the console, not persisted in GSSO DB
Personal dataIDNP encrypted (AES-GCM, key from secret store) and masked; logs scrubbed
Events integrityHash chain + DB trigger + GLog WORM copy
KeysRS256 realm keys, rotation 90 days with overlap (new active, old passive until max token TTL passes)
Supply chainSBOM (CycloneDX), Trivy, Semgrep, Gitleaks via /gsast; Keycloak image built from pinned digest

GSSO console roles (realm gstack):

RoleSensitive
GSSO_ADMINyes
GSSO_REALM_ADMINyes
GSSO_PLATFORM_OWNERyes
GSSO_APPROVERyes
GSSO_HELPDESKyes
GSSO_AUDITORno
GSSO_IDNP_VIEWyes
GSSO_MFA_REQUIREDmarker
  • Compose stack ~/devops/gsso/ on the shared host. The containers join the external gstack-web network.
  • The live keycloak + postgres-keycloak containers are taken over (§11.1): the stock image is swapped for the GSSO image, built on the same version with extensions and theme added.
  • gsso, gsso-gateway, gsso-web, gsso-postgres and, if no shared Kafka is available, a single-node Kafka (KRaft).
  • vhosts:
    • sso.gstack.esempla.systems → keycloak:8080, as today;
    • new gsso.gstack.esempla.systems → gsso-web + gateway. Any edge nginx change requires the owner’s approval, and certificate renewal must be fixed first (NFR-AVL-006).
  • Deploy flow: build amd64 images → push to registry.esempla.systems/govtech/gstack/gsso-* (or docker save | ssh | docker load) → docker compose up -d --no-deps --force-recreate <svc>.
  • Memory budget on the ~8 GB host:
    • Keycloak 1 GB heap;
    • gsso 512 MB;
    • gateway 384 MB;
    • postgres ×2 256 MB each;
    • Kafka 512 MB, if local.
  • Keycloak: 2 replicas (Infinispan cluster through the Kubernetes DNS_PING), with PDB and HPA on CPU.
  • gsso: 2 replicas. The reconciler is singleton per realm through the advisory lock, so both replicas can run.
  • gateway: 2 replicas, sessions in Redis or JDBC.
  • PostgreSQL: CloudNativePG, 2 instances each.
  • Kafka: the shared Strimzi cluster.
ItemEstimate
Staff users5 000 (initial) → 20 000
Peak logins50/min typical, 10/s burst at 9:00
Events/day≈ 50 000 (logins, token refresh not logged per default, admin) → ≈ 30 MB/day jsonb → 90-day partitions ≈ 3 GB
GSSO DB< 10 GB year one
Keycloak DB< 5 GB (sessions in memory, persistent user sessions feature enabled in 26.x → +1 GB)
  • Metrics (Micrometer → Prometheus):
    • gsso_sync_jobs{state}, gsso_reconcile_seconds, gsso_drift_total, gsso_event_lag_seconds, gsso_glog_lag_seconds;
    • Keycloak keycloak_logins_total, keycloak_failed_login_attempts_total (metrics SPI);
    • JVM and HTTP metrics.
  • Logs: JSON with trace id; scrubbing filter for Authorization, access_token, client_secret, password, idnp.
  • Tracing: OpenTelemetry across gateway → gsso → Keycloak admin calls.
  • Alerts: NFR-OBS-004, routed through GNotify.
  1. Get owner approval and agree a window. Fix TLS renewal first.
  2. pg_dump the Keycloak database and copy it off-host. Export each realm (kc.sh export --realm … --users different_files) for reference.
  3. Build the GSSO Keycloak image on the same version (26.6.3) with extensions and theme added. No baseline is applied yet.
  4. Swap the image and verify:
    • logins for all apps;
    • the issuer is unchanged;
    • JWKS is unchanged. Rollback = previous image, same database.
  5. Enable the gsso-kafka listener in realm interdictii (admin action), then start gsso with realm interdictii adopted, IGNORE.
  6. Apply the gstack and cetatean baselines (new realms; nothing existing changes).
RunbookSteps
Keycloak upgradeBack up the database → run the baseline + reconciler IT suite against the new image → staged rollout → smoke test
Key rotationAdd a new RS256 key (higher priority) → wait at least 1 token TTL + refresh → set the old key passive → remove it after the max session
Client secret rotationConsole “rotate” → dual-secret grace window → app redeploy → old secret invalidated
Break-glassUse the master emergency admin (sealed credentials, two-person rule); every use is reported
RestoreRestore the Keycloak database from backup → run a full reconcile in REPORT → review drift → ENFORCE
CertificateRenew via webroot; alert at 21 days; monthly check job
LevelScopeTooling
UnitGrant state machine, approval policy, differ, hash chain, mappers, starter mappingJUnit 5, AssertJ
IntegrationReconciler against real Keycloak, event listener → Kafka → consumer, Liquibase + append-only triggerTestcontainers (Keycloak 26.6.x with extensions, PostgreSQL, Kafka) and deploy/docker-compose.test.yml (down -v always)
Contractapi/v1/app OpenAPI, event schemas (CloudEvents + JSON Schema)api_audit.py, schema tests
E2EConsole flows (register platform, user, grant approval, revoke, drift), SSO across two sample apps, MPass mockCypress
SecurityAuthorization matrix (realm/platform scoping, four-eyes), ZAP baseline, headers, SAST/SCA/secrets/gtestgen, /gtest, /gsast
PerformanceLogin burst, console lists, reconcile of 1 000 clients/load-test, k6 for Keycloak
UI sweepDead controls, untranslated keys, a11y/gfront

Test scenarios are listed in test-scenarios.md (TS-GSSO-NN) and reuse the shared TC-COM-* cases.

ItemEstimate
Demo hostFits on the existing host (≈ 3 GB additional RAM)
Kubernetes target3 vCPU / 6 GB requests (Keycloak 2×, gsso 2×, gateway 2×) + 2 PostgreSQL clusters
Build effort (S0–S3)≈ 14–18 person-weeks (1 backend, 1 frontend, 0.5 Keycloak/DevOps)
Migration of existing apps (S4)≈ 1–2 person-weeks per pattern-A app, ≈ 0.5 per pattern-B app