Multi-Tenant Architecture
How CONFIG365 uses a single multi-tenant app registration to manage multiple Microsoft 365 tenants with centralized credentials and per-tenant admin consent.
Overview
CONFIG365 uses a single multi-tenant app registration hosted in your MSP's Entra ID tenant. This one app can authenticate to any number of customer tenants — each customer simply grants admin consent once, and no per-tenant app registration is ever needed.
Architecture Components
1. Single Multi-Tenant App Registration
Located in your MSP's Entra ID tenant. One Client ID and secret shared across all managed tenants.
Name: M365-Automation Sign-in Audience: AzureADMultipleOrgs (multi-tenant) Permissions: Microsoft Graph API (Application)
2. Per-Tenant Admin Consent
Each tenant's Global Administrator visits a consent URL once. After consent, a Service Principal is created in their tenant — your app can then authenticate to it.
https://login.microsoftonline.com/organizations/adminconsent?client_id={your-app-id} 3. Credentials in CONFIG365
Credentials are managed at two levels inside CONFIG365:
MSP-level Gitea org secrets (set automatically by the portal)
| Secret | Value |
|---|---|
AZURE_CLIENT_ID | App Client ID (shared across all tenants) |
MSP_ORG | MSP Gitea org name |
PLATFORM_ORG | Platform (config365) Gitea org name |
CONFIG365_TOKEN | Gitea admin token for pipeline operations |
Per-tenant credentials (stored encrypted in SQLite, resolved at runtime)
| Field | Value |
|---|---|
tenantId | Tenant's Entra ID tenant ID |
clientId | App Client ID (or per-tenant override) |
slug | Short name used as Gitea repo prefix (e.g. contoso) |
exchangeOrgName | contoso.onmicrosoft.com (optional) |
The token-api service running on localhost:4322 inside the container resolves per-tenant tokens at pipeline runtime — credentials never appear in workflow YAML.
Authentication Flow
MSP Entra ID Tenant
└── Multi-Tenant App Registration (delegated)
App ID: 12345678-...
Allow public client flows: Yes
│
│ Admin consent + device-code sign-in (once per tenant)
▼
Tenant A (ID: 111...) Tenant B (ID: 222...)
Delegated refresh token Delegated refresh token
Same App ID Same App ID
│
▼
Pipelines call token-api (localhost:4322)
Graph → GRAPH delegated scopes
Exchange → outlook.office365.com/.default (Exchange.Manage)
SharePoint → {tenant}.sharepoint.com/.default (AllSites.FullControl)
MDE → api.securitycenter.microsoft.com/.default (Machine.Read) Security Model
Credential Protection
Do
- Store Client Secret in the CONFIG365 portal — it encrypts it in SQLite automatically
- Rotate secrets every 12–24 months and update via the portal
- Use a separate app registration for dev vs. production
Don't
- Never commit secrets to Git
- Don't share secrets via email or chat
- Don't grant more permissions than needed
Tenant Isolation
- – Each tenant has its own Git repository and pipeline
- – Each pipeline specifies the tenant's unique Tenant ID — accidental cross-tenant deployment is impossible
- – Backups are stored per-tenant in their own repository
- – Tenant admins can revoke consent at any time from their own Entra portal
Credential Rotation
Entra admin center → App registrations → Your app → Certificates & secrets → New client secret (24 months)
Open Settings → MSP App Registration and paste the new Client Secret — the portal re-encrypts and updates Gitea org secrets automatically
Trigger a deploy for a test tenant from the portal dashboard and verify successful authentication
Because credentials are resolved at runtime by the token-api, every subsequent pipeline run uses the new secret without any per-tenant changes
After confirming the new secret works, remove the old one from app registration
Multi-Tenant vs. Per-Tenant Apps
| Aspect | Multi-Tenant (recommended) | Per-Tenant Apps |
|---|---|---|
| Number of Apps | 1 | One per tenant |
| Credential Management | Centralized | Distributed |
| Secret Rotation | Update once | Update N times |
| Setup Complexity | Low | High |
| Ongoing Maintenance | Low | High |
| Tenant Onboarding | Send consent URL | Create new app |
| Scalability | Excellent | Poor |
Tenant-Specific App Override
For special cases — GCC High, dedicated security requirements, or tenants that need their own app registration — you can override the shared credentials on a per-tenant basis. Set the following fields in the tenant's settings in the CONFIG365 portal:
| Variable | Value | Secret |
|---|---|---|
Tenant-clientId | Tenant's app Client ID | 🔒 Yes |
Tenant-clientSecret | Tenant's app Client Secret | 🔒 Yes |
When a per-tenant clientId and clientSecret are saved in the tenant's portal settings, the token-api automatically uses them instead of the shared MSP credentials. No workflow YAML changes are needed.
Troubleshooting
AADSTS650053: The application is asking for permissions that weren't granted
Cause: Tenant has not granted admin consent
- – Generate consent URL and send to tenant's Global Admin
- – Wait for consent confirmation before running the pipeline
AADSTS7000218: The request body must contain client_assertion or client_secret
Cause: Client secret is missing or incorrect
- – Verify the Client Secret is saved in the CONFIG365 portal (Settings → MSP App Registration)
- – Confirm the portal has successfully synced the secret to Gitea org secrets
- – Check if the secret has expired in the Entra admin center
AADSTS90002: Tenant not found
Cause: Incorrect Tenant ID saved in the portal
- – Verify the tenant ID in the CONFIG365 portal tenant settings
- – Confirm the tenant has not moved tenants
- – Check for typos
Failed to acquire token
Cause: Consent may have been revoked
- – Check Entra ID → Enterprise Applications for your app
- – If missing, generate a new consent URL and re-consent