Security and Compliance

Overview

Membership processes sensitive personal data (names, addresses, bank accounts, health-related information) for potentially millions of members across hundreds of organizations. The security architecture is designed to meet GDPR requirements, eIDAS electronic signature standards, and OWASP Application Security Verification Standard (ASVS) Level 2. Every design decision in this chapter is informed by the 196 findings from the Cash360 Phase 4 security audit, ensuring that known vulnerabilities are not repeated.


GDPR Compliance

Data Protection Principles

GDPR Principle Implementation
Lawfulness Every data processing operation linked to a legal basis (contract, consent, legitimate interest). Consent records stored with timestamp and version.
Purpose limitation Data collection fields tagged with processing purpose. No silent data enrichment.
Data minimization Registration forms collect only essential fields. Optional fields clearly marked. Custom attributes are tenant-configured, not platform-mandated.
Accuracy Members can self-service update their profiles. Admin-modified fields carry audit trail.
Storage limitation Configurable data retention policies per entity. Automated purge jobs for expired data.
Integrity and confidentiality Encryption at rest (AES-256) and in transit (TLS 1.2+). Access controls on all personal data.

Every data processing activity requires documented consent. The system maintains a consent registry:

Consent record:
- Member ID
- Consent type (marketing_email, data_sharing, photo_publication, health_data, etc.)
- Granted: true/false
- Granted at: timestamp
- Consent version: "v2.1" (links to consent text version)
- IP address at time of consent
- Revoked at: timestamp (if applicable)

Consent can be granted during registration, membership purchase, or through the member profile. Withdrawal of consent triggers immediate cessation of the associated processing activity.

Right to Erasure (Art. 17)

When a member requests data deletion:

  1. Validation: Verify identity of the requesting person (email confirmation + authentication)
  2. Scope assessment: Determine which data can be deleted vs. must be retained (legal obligations: tax records for 10 years, contract documents for 6 years under German commercial law)
  3. Soft delete: Member profile is anonymized (personal fields replaced with "[DELETED]", email hashed) rather than hard-deleted, preserving referential integrity
  4. Cascade: All linked documents deleted from object storage. All linked communications purged. CheckIn history anonymized (member reference removed, statistical data retained).
  5. Third-party notification: Cash360 notified to anonymize related transaction records. External services (email providers, push services) instructed to purge subscriber data.
  6. Confirmation: Deletion certificate generated and sent to the member.

Data Export (Art. 20 -- Right to Data Portability)

The endpoint GET /api/account/export-data generates a machine-readable export of all personal data:

Response format: JSON file containing: - Personal profile data - Contract history - Transaction history - Bank account data (masked: only last 4 digits of IBAN) - Check-in history - Communication history (sent to this member) - Consent records - Custom attribute data

The export is generated asynchronously (large datasets) and delivered via a time-limited download link sent to the member's verified email address.

Data Protection Impact Assessment (DPIA)

The following processing activities require a DPIA:

  • Health-related data processing (medical certificates, body composition data)
  • Biometric data processing (fingerprint check-in, if enabled)
  • Systematic monitoring (check-in tracking, attendance analytics)
  • Large-scale processing of children's data (youth sports clubs)
  • Automated decision-making (dunning workflows, contract suspension)

eIDAS-Compliant Contract Conclusion

Membership contracts are legally binding documents. The platform supports electronic signatures compliant with the eIDAS Regulation (EU 910/2014):

Signature Levels

Level Use Case Implementation
Simple Electronic Signature (SES) Standard membership contracts Click-to-sign with audit trail (timestamp, IP, user agent)
Advanced Electronic Signature (AES) High-value contracts, minors Signature pad capture + identity verification
Qualified Electronic Signature (QES) Legal disputes, franchise agreements Integration with qualified trust service provider (e.g., DocuSign, Swisscom)

Contract Signing Flow

sequenceDiagram participant MB as Member participant APP as Membership App participant SRV as Membership Server participant TSP as Trust Service Provider MB->>APP: Select membership plan APP->>SRV: POST /api/contract/purchase SRV->>SRV: Generate contract PDF SRV-->>APP: Contract preview + signature request MB->>APP: Sign (touch/click) APP->>SRV: POST /api/contract/sign (signature data) alt Simple Signature SRV->>SRV: Store signature + audit trail else Qualified Signature SRV->>TSP: Request QES TSP-->>SRV: Signed document end SRV->>SRV: Store signed contract as Document SRV-->>APP: Contract confirmed SRV->>MB: Email with signed PDF

For minors (under 18), the contract requires the signature of the responsible person (parent/guardian). The system enforces this by requiring idResponsiblePerson on the Member record and routing the signature request to the responsible person's email.


Zero-Trust Architecture

The platform follows zero-trust principles: never trust, always verify. No network location, device, or user is inherently trusted.

Principles Applied

  1. Verify explicitly: Every API request authenticated and authorized, even internal service-to-service calls
  2. Least privilege access: Users receive the minimum permissions required for their role. Admins do not have blanket access to all data.
  3. Assume breach: All data encrypted at rest. Audit logging on all sensitive operations. Session tokens expire aggressively.

Network Architecture

graph TB subgraph "Public Internet" MOBILE[Mobile Apps] WEB[Web Browsers] end subgraph "Edge Layer" CDN[CDN / WAF] LB[Load Balancer] end subgraph "Application Layer" GW[API Gateway] AUTH[Auth Service] APP[Application Services] end subgraph "Data Layer" DB[(PostgreSQL)] CACHE[(Redis)] STORE[(Object Storage)] MQ[(RabbitMQ)] end MOBILE --> CDN --> LB --> GW WEB --> CDN --> LB --> GW GW --> AUTH GW --> APP APP --> DB APP --> CACHE APP --> STORE APP --> MQ style DB fill:#fdd style CACHE fill:#fdd style STORE fill:#fdd style MQ fill:#fdd
  • The API Gateway validates JWT tokens before forwarding requests
  • The Data Layer is not accessible from the public internet
  • Internal services communicate over mTLS
  • Database connections use SSL with certificate-based authentication

Authentication

JWT Token Architecture

Membership uses a dual-token strategy with short-lived access tokens and long-lived refresh tokens:

Token Lifetime Storage (Mobile) Storage (Web)
Access token 15 minutes In-memory only In-memory only
Refresh token 30 days AES-256 encrypted (Android Keystore / iOS Keychain) HttpOnly + Secure + SameSite=Strict cookie

Critical lesson from Cash360 audit: Cash360 used a hardcoded JWT signing key ("springboot-core") and stored it in application properties. Membership uses:

  • Asymmetric keys (RS256): Private key for signing (server only), public key for verification (can be distributed to API consumers)
  • Key rotation: Keys rotated every 90 days. Old keys remain valid for verification until all access tokens issued with them expire.
  • Key storage: Private key stored in environment variable or secrets manager, never in source code or config files.

Access Token Claims

{
  "sub": "user-42",
  "iss": "membership.example.com",
  "aud": "membership-api",
  "iat": 1740000000,
  "exp": 1740000900,
  "eid": 1,
  "role": "ADMIN",
  "mid": 42,
  "jti": "unique-token-id-uuid"
}
  • eid: Entity ID (tenant context)
  • role: User role
  • mid: Member ID (null for admin-only users)
  • jti: Unique token ID for revocation tracking

Refresh Token Rotation

Every time a refresh token is used, a new refresh token is issued and the old one is invalidated. If a refresh token is reused (indicating it was stolen), all tokens for that user are revoked and the account is flagged for review.

sequenceDiagram participant C as Client participant S as Server Note over C: Access token expired C->>S: POST /api/auth/refresh (refresh_token_v1) S->>S: Validate refresh_token_v1 S->>S: Invalidate refresh_token_v1 S->>S: Generate refresh_token_v2 S->>S: Generate new access_token S-->>C: access_token + refresh_token_v2 Note over C: Later: attacker tries stolen token C->>S: POST /api/auth/refresh (refresh_token_v1) S->>S: Token already used - THEFT DETECTED S->>S: Revoke ALL tokens for user S-->>C: 401 + account flagged

Registration and Verification

  1. Member submits registration: POST /api/auth/register (email, password, entity code)
  2. Server creates User with statusCd = PENDING_VERIFICATION
  3. Verification email sent with time-limited token (24 hours)
  4. Member clicks link: GET /api/auth/verify?token=...
  5. Server activates user: statusCd = ACTIVE
  6. If token expires: POST /api/auth/resend-verification

Password Policy

Password requirements are configurable per entity (stored in Entity settings JSONB):

{
  "passwordPolicy": {
    "minLength": 10,
    "requireUppercase": true,
    "requireLowercase": true,
    "requireDigit": true,
    "requireSpecialChar": true,
    "maxRepeatingChars": 3,
    "passwordHistoryCount": 5,
    "expirationDays": 0
  }
}

Default policy enforces minimum 10 characters with mixed case, digit, and special character. Password history prevents reuse of the last 5 passwords.

Brute-Force Protection

  • After 5 consecutive failed login attempts: account locked for 15 minutes
  • After 10 consecutive failed attempts: account locked for 1 hour
  • After 20 consecutive failed attempts: account locked until manual admin intervention
  • Failed attempt counter resets on successful login
  • Rate limiting at API Gateway level: max 10 login attempts per IP per minute
  • CAPTCHA challenge after 3 failed attempts from the same IP

Authorization (RBAC)

Role Hierarchy

graph TB SA[SYSTEM_ADMIN] --> GA[GROUP_ADMIN] GA --> A[ADMIN] A --> T[TRAINER] T --> M[MEMBER]
Role Scope Key Permissions
MEMBER Own data View/edit own profile, view own contracts, view own transactions, check-in, book courses
TRAINER Assigned courses + member data Mark attendance, view participant profiles, manage assigned courses, view resource bookings
ADMIN Single entity Full CRUD on members, contracts, products, resources, events. User management. Financial reports. Settings.
GROUP_ADMIN Parent + child entities Everything ADMIN can do, plus: cross-entity reporting, manage child entity settings, create/deactivate child entities
SYSTEM_ADMIN All entities Platform administration, tenant management, system configuration, global monitoring

Permission Enforcement

Permissions are checked at two levels:

  1. Method level: @PreAuthorize("hasRole('ADMIN') or hasRole('GROUP_ADMIN')") on controller methods
  2. Data level: Repository queries filtered by tenant context. An ADMIN of Entity 1 cannot access data of Entity 2 even if they craft a direct API call with Entity 2's IDs.

Admin roles are scoped to their entity. A GROUP_ADMIN at a parent entity can access child entity data through explicit parent-child traversal endpoints, never through direct entity ID injection.


Transport Security

Measure Implementation
TLS TLS 1.2+ required for all connections. TLS 1.0/1.1 rejected.
HSTS Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Certificate pinning Mobile apps pin the API server's certificate chain (backup pins included for rotation)
CORS Explicit origin whitelist per entity's configured domain. No wildcards. (Cash360 used *, which is a P0 vulnerability.)
CSP Content Security Policy headers on all web responses
Cookie security Secure; HttpOnly; SameSite=Strict on all auth cookies

Input Validation

All input validation happens at the API boundary (controller layer) before reaching business logic:

Validation Implementation
Request body Jakarta Bean Validation (@NotNull, @Size, @Email, @Pattern) on all DTOs
Path parameters Type-safe binding + range checks
IBAN validation Checksum validation (ISO 13616) + bank directory lookup for BIC auto-fill
Email validation RFC 5322 format + MX record check (no disposable email domains)
Phone validation libphonenumber for international format validation
File upload MIME type validation, max size enforcement (10 MB default), virus scan via ClamAV
SQL injection Parameterized queries only (JPA/Hibernate). No string concatenation in queries.
XSS HTML sanitization on all text inputs. Output encoding in templates.
JSONB injection Schema validation on all JSONB inputs against entity's custom attribute definitions

Audit Logging

Every security-relevant action is logged to the AuditLog table (immutable, append-only):

Logged actions:

Category Actions
Authentication Login, logout, failed login, password change, password reset, MFA enable/disable
Authorization Permission denied, role change, user creation/deactivation
Data access Member profile view (by admin), financial data export, bulk data query
Data modification Create, update, delete on any business entity
Configuration Entity settings change, role assignment, integration setup
Financial Transaction creation, billing run, SEPA export, refund
Compliance Data export request, erasure request, consent change

Log format:

{
  "timestamp": "2026-01-15T14:30:00Z",
  "userId": 42,
  "entityId": 1,
  "action": "UPDATE",
  "entityType": "Member",
  "entityId": 99,
  "changes": {
    "email": {"old": "old@example.com", "new": "new@example.com"}
  },
  "ipAddress": "192.168.1.100",
  "userAgent": "MembershipApp/1.0 (Android 14)"
}

Audit logs are retained for 7 years (configurable per entity to meet local regulations). They are stored in a separate database partition for performance and can be exported for compliance audits.

Critical rule: Personal data (PII) is never logged in plaintext in application logs (console/file). Only the AuditLog table contains PII, and access to it requires ADMIN role.


ADR-AUTH-001: Custom Authentication vs. Keycloak

Status

Accepted (February 2026)

Context

The Membership platform requires authentication and authorization for multiple user types (members, admins, trainers, franchise directors) across a multi-tenant SaaS environment. The decision is whether to:

  • (A) Build custom authentication in the membership-auth module (Spring Security + JWT RS256), or
  • (B) Deploy Keycloak (self-hosted, Bitwarden-compatible OpenID Connect / OAuth2 identity provider) as the primary identity system.

The team is small (4 developers), the infrastructure budget is constrained (~EUR 100/month on Hetzner Cloud), and the initial target market consists of small to medium studios and clubs that need simple email/password authentication.

Decision

Option A for v1.0: Custom authentication (membership-auth module) with an IdP abstraction layer that enables Keycloak integration in v2.0+ for Enterprise customers.

Rationale

Why custom auth for v1.0

Factor Custom Auth Keycloak
Tenant-aware JWT claims Native — eid (entity), role, mid (member) baked into token generation Requires custom Protocol Mapper SPIs and Keycloak extensions
Infrastructure cost Zero — runs in the existing API pods +1–2 GB RAM, own PostgreSQL DB, ~EUR 10–15/month additional
Operational complexity Part of the monolith, same deployment pipeline Separate system: own admin console, realm management, theme customization, patching
Team expertise Spring Security is well-known to the team Keycloak SPI development requires specialized knowledge
v1.0 target users Small studios and clubs need email + password, nothing more SSO/SAML/LDAP are irrelevant for this segment
Time to market Auth module already designed, standard Spring Security patterns Keycloak setup, customization, and integration adds 2–4 weeks

Why Keycloak becomes relevant for v2.0+

Enterprise Feature Needed From Justification
SAML/OIDC SSO v2.0 (Enterprise tier) Fitness chains (50+ locations) with corporate IT require federation with their Active Directory or Azure AD
Social Login (Google, Apple) v2.0 (all tiers) Members expect one-click registration via their existing accounts
WebAuthn / FIDO2 (Passkeys) v2.0 (all tiers) Passwordless authentication for admin panels and mobile apps
LDAP/AD Federation v2.0 (Enterprise tier) Large organizations with existing employee directories
Identity Brokering v3.0+ Multi-IdP scenarios (franchise with different identity providers per franchisee)

Architecture: IdP Abstraction Layer

The membership-auth module exposes an internal IdentityProvider interface that abstracts the authentication source. In v1.0, the sole implementation is the built-in LocalIdentityProvider. In v2.0+, a KeycloakIdentityProvider can be added without changing any downstream code.

// Identity Provider abstraction (membership-auth)
public interface IdentityProvider {
    AuthResult authenticate(AuthRequest request);
    TokenPair issueTokens(AuthenticatedUser user);
    AuthenticatedUser validateToken(String accessToken);
    void revokeTokens(String userId);
    UserProfile getUserProfile(String userId);
}

// v1.0: Built-in implementation
@Component
@ConditionalOnProperty(name = "auth.provider", havingValue = "local", matchIfMissing = true)
public class LocalIdentityProvider implements IdentityProvider {
    // JWT RS256, bcrypt passwords, Redis-based brute-force protection
    // Tenant-aware claims (eid, role, mid) — native
}

// v2.0+: Keycloak implementation (Enterprise tier)
@Component
@ConditionalOnProperty(name = "auth.provider", havingValue = "keycloak")
public class KeycloakIdentityProvider implements IdentityProvider {
    // Delegates to Keycloak via OpenID Connect
    // Maps Keycloak tokens to internal AuthenticatedUser with eid/role/mid claims
    // Keycloak realm per tenant (entity) or single realm with group-based isolation
}

Token flow with Keycloak (v2.0+):

sequenceDiagram participant C as Client (App) participant API as Membership API participant KC as Keycloak participant DB as PostgreSQL alt v1.0 — Local Auth C->>API: POST /api/auth/login (email, password) API->>DB: Validate credentials API->>API: Generate JWT (eid, role, mid) API-->>C: Access token + Refresh token else v2.0+ — Keycloak (Enterprise) C->>KC: Login (OIDC / SAML / Social) KC-->>C: Keycloak access token C->>API: Request with Keycloak token API->>KC: Validate token (JWKS) API->>DB: Resolve eid, role, mid from user mapping API->>API: Enrich SecurityContext with tenant claims API-->>C: Response end

Keycloak deployment (v2.0+): - Self-hosted on Hetzner Cloud (dedicated CX22 node, ~EUR 5–10/month) - Keycloak 26+ (Quarkus-based, reduced footprint vs. legacy WildFly) - One realm per Enterprise customer (tenant isolation at identity level) - Team/Starter/Professional customers continue using LocalIdentityProvider - Enterprise customers choose their auth method during onboarding: local, Keycloak SSO, or federated (SAML/LDAP)

Consequences

Positive: - v1.0 ships faster with proven Spring Security patterns — no Keycloak complexity - Infrastructure costs remain at ~EUR 100/month for v1.0 - The IdP abstraction layer ensures clean separation — Keycloak can be introduced without refactoring business logic - Enterprise SSO becomes a premium upsell feature (justifies Enterprise pricing)

Negative: - Social Login (Google, Apple) is not available in v1.0 — must be built or deferred to Keycloak in v2.0 - MFA (TOTP/WebAuthn) must be implemented in-house for v1.0 (standard Spring Security + aerogear-otp pattern from Cash360) - The IdP abstraction adds a thin layer of indirection that would not exist with a Keycloak-first approach

Risks: - If an Enterprise customer requires SSO before v2.0 is ready, a fast-track Keycloak deployment may be needed - The IdP abstraction must be respected in all auth-related code — any direct coupling to LocalIdentityProvider creates technical debt

Review Date

Re-evaluate when the first Enterprise customer requests SSO/SAML, or when v2.0 planning begins — whichever comes first.


Lessons from the Cash360 Security Audit

The Cash360 Phase 4 audit identified 196 findings (25 P0 Critical, 45 P1 High). The following table maps the most critical findings to Membership's mitigation strategy:

Cash360 Finding Severity Membership Mitigation
Hardcoded JWT signing key "springboot-core" P0 RS256 asymmetric keys, stored in secrets manager, rotated every 90 days
CORS wildcard * on all endpoints P0 Explicit per-entity origin whitelist, no wildcards
Missing authentication on public endpoints P0 All endpoints require authentication except: login, register, verify, forgot-password, public entity info
No @Version on any generated entity P0 @Version on every entity (optimistic locking) from day one
Race conditions in billing (HashMap, no DB locking) P0 SELECT ... FOR UPDATE SKIP LOCKED on billing cycle, ConcurrentHashMap where needed
Double used for financial amounts P1 BigDecimal / DECIMAL(19,4) exclusively for all monetary values
Missing @Transactional on batch operations P1 All service methods with DB mutations annotated with @Transactional
Unguarded Optional.get() calls P1 Strict code review rule: use orElseThrow() with meaningful exception
Passwords stored with weak algorithm P1 bcrypt with cost factor 12 (configurable)
Hardcoded EBICS/SMTP credentials in config P1 All credentials in environment variables or secrets manager

Encryption

At Rest

Data Encryption
Database PostgreSQL TDE (Transparent Data Encryption) or volume-level encryption
Object storage AES-256 server-side encryption
Backups Encrypted before transfer, stored encrypted
Sensitive fields (MFA secrets, tokens) Application-level AES-256 encryption before DB storage

In Transit

Path Encryption
Client to API TLS 1.2+ (HTTPS)
API to Database SSL with certificate authentication
API to Cash360 TLS 1.2+ with certificate pinning
API to external services TLS 1.2+
Internal service-to-service mTLS

Key Management

  • Encryption keys managed by Hetzner Cloud Secrets or Kubernetes Sealed Secrets (SOPS), with HashiCorp Vault as option for on-premises
  • Key rotation: encryption keys rotated annually, re-encryption of existing data performed during maintenance windows
  • Separation of duties: different keys for different data categories (authentication, financial, personal)

Team Credential Management (Vaultwarden)

The development team and operations staff use Vaultwarden (self-hosted, Bitwarden-compatible) as the team password manager for all human-accessed credentials:

Credential Type Example Storage
Service account passwords Hetzner Cloud console, GitLab admin, DATEV portal Vaultwarden (shared vault)
API keys (external) My-Factura API key, Cloudflare API token, SMTP credentials Vaultwarden + Kubernetes Sealed Secrets
Infrastructure access SSH keys, Kubernetes kubeconfig, database admin passwords Vaultwarden (restricted vault)
Third-party logins Domain registrar, IHK portal, Finanzamt ELSTER, insurance portals Vaultwarden (CEO vault)
Recovery codes MFA backup codes, root account recovery tokens Vaultwarden (emergency vault)

Vaultwarden deployment: - Self-hosted on Hetzner Cloud (Docker container on infra node), accessible only via VPN or admin IP allowlist - Encrypted with master password + TOTP-based MFA for all team members - Automatic nightly backup to Object Storage (encrypted) - Bitwarden client apps (desktop, mobile, browser extension) for team access - Organization vaults with role-based sharing (CEO, Dev Lead, Developer)

Distinction: Vaultwarden manages human-accessed credentials (admin panels, third-party portals). Kubernetes Sealed Secrets / SOPS manages application-injected secrets (database passwords, JWT keys, API keys injected as environment variables). Both systems complement each other — Vaultwarden for people, Sealed Secrets for pods.


Software License Compliance

All tools and libraries used by the Membership One platform are reviewed for license compatibility with commercial SaaS distribution. The following table lists every significant component with its license type and commercial use assessment.

License Inventory

Component Product License Commercial Use Notes
Runtime Java 25 (Eclipse Temurin) GPLv2 + Classpath Exception Yes Free for commercial use
Framework Spring Boot 4.0 Apache 2.0 Yes
Build Apache Maven Apache 2.0 Yes
Database PostgreSQL 18 PostgreSQL License (MIT-like) Yes
Cache Redis 7 RSALv2 / SSPLv1 (since Redis 7.4) Yes* *Server-side license changed; client libraries remain MIT. Self-hosted use is permitted.
Messaging RabbitMQ 4 MPL 2.0 Yes
Search Elasticsearch 8 SSPLv1 / Elastic License 2.0 Yes* *Self-hosted use permitted; cannot offer as a service. Consider OpenSearch (Apache 2.0) as alternative.
ORM Hibernate 6.x LGPL 2.1 Yes
Migrations Flyway Apache 2.0 (Community) Yes Community edition sufficient
Security Spring Security Apache 2.0 Yes
JWT auth0 java-jwt 4.x MIT Yes
PDF Open HTML to PDF LGPL 2.1 Yes
API Docs SpringDoc OpenAPI Apache 2.0 Yes
Annotations Lombok MIT Yes
JSON Jackson Apache 2.0 Yes
Resilience Resilience4j Apache 2.0 Yes
Utilities Guava Apache 2.0 Yes
Testing JUnit 5 EPL 2.0 Yes
Testing Mockito MIT Yes
Testing Testcontainers Apache 2.0 Yes
Testing WireMock Apache 2.0 Yes
Coverage JaCoCo EPL 2.0 Yes
Frontend Flutter / Dart BSD 3-Clause Yes
State Riverpod MIT Yes
HTTP Dio MIT Yes
Routing GoRouter BSD 3-Clause Yes
Code Gen Freezed MIT Yes
Monitoring Prometheus Apache 2.0 Yes
Dashboards Grafana AGPL 3.0 Yes* *Self-hosted use permitted; modifications must be shared if offered as a service
Logging Loki AGPL 3.0 Yes* *Same as Grafana
Monitoring Icinga GPL 2.0 Yes
ACME Dehydrated MIT Yes
Passwords Vaultwarden AGPL 3.0 Yes* *Self-hosted use permitted
CI/CD GitLab CE MIT (CE) Yes Community Edition
Container Docker Apache 2.0 Yes
Orchestration Kubernetes Apache 2.0 Yes
CDN/WAF Cloudflare Proprietary (Free Plan) Yes Free tier sufficient for initial operations
Infrastructure Hetzner Cloud Proprietary (IaaS) Yes Pay-per-use, no license concerns

License Assessment

All core components use permissive open-source licenses (Apache 2.0, MIT, BSD) that allow unrestricted commercial use. Components under AGPL 3.0 (Grafana, Loki, Vaultwarden) are self-hosted infrastructure tools -- AGPL obligations apply only if modifications are distributed as a service, which is not the case here. The Redis license change (RSALv2/SSPLv1 since 7.4) and Elasticsearch license (SSPL/Elastic License 2.0) permit self-hosted commercial use but prohibit offering them as a managed service to third parties.

Ongoing Compliance

License compliance is audited annually in Q1 via automated dependency scanning in the CI/CD pipeline (see Chapter 16, Ongoing Compliance). Any license change in upstream dependencies triggers a review before the next release.