A Backstage developer portal holds integration credentials for source control, CI, cloud and monitoring systems, runs plugins with no boundary between them, and by default lets every signed-in user do everything. That combination turns an internal convenience into one of the most connected systems in the engineering estate. This guide explains how identity works in Backstage (sign-in resolvers, Backstage tokens and plugin-to-plugin auth), how to lock down the permission framework, catalog and Scaffolder, how to handle the integration secrets plugins depend on, and what to log. It is for platform engineering teams who run Backstage and the security engineers who review it. Behaviour below was checked against the Backstage documentation in the backstage/backstage repository on 27 October 2026 and assumes the new backend system (release 1.24 or later).
Key takeaways
- Signed in means full access until you add a policy. The Backstage threat model states that signed-in users generally have full access to all information and actions; endpoints are unprotected by authorisation and an allow-all policy ships by default.
- Plugins are not sandboxed. Any plugin on the same host, or with access to another plugin's database, should be treated as having full access to that plugin, its configuration and its secrets. Vet plugins like any npm dependency.
- The sign-in resolver is the identity boundary. A wrong mapping from identity provider to User entity is account takeover. Use one resolver, never guest sign-in in production, and only signature-verifying proxy providers.
- External tokens need access restrictions. A static token in
backend.auth.externalAccesswith noaccessRestrictionshas unlimited access to all functionality of all plugins. - Keep it off the internet. Backstage is designed for a protected environment. Put it behind an authenticating proxy and restrict
backend.reading.allowso the backend cannot be used to read cloud metadata endpoints.
How identity and access work in Backstage
The project's threat model divides people into trust groups, and it is worth adopting the same language in your own risk register.
| Group | Who | Trust level in the threat model | Your control |
|---|---|---|---|
| Internal user | Authenticated members of the organisation | Not expected to attack availability, but not trusted to protect confidentiality or integrity | Permission policy, catalog rules, Scaffolder restrictions |
| Operator | People who configure and run the instance | Fully trusted; effectively root on the host and database | PAM for hosts, Kubernetes and database; change review on config |
| Builder | Internal or external code contributors, including plugin authors | Similar access to operators, because plugin code runs with full access | Code review, dependency vetting, separate deployments for sensitive plugins |
| External user | Anyone outside the organisation | Assumed to have no direct access at all | Network isolation and an authenticating proxy |
Sign-in and Backstage tokens
- Auth providers: the
authplugin acts as an OAuth 2.0 authorisation server for provider integrations (GitHub, Microsoft, Okta, Google and others). All providers are disabled by default; enable only those you use. - Sign-in resolvers: each provider used for sign-in needs a resolver that maps the external identity to a User entity in the catalog, for example
userIdMatchingUserEntityAnnotation,emailMatchingUserEntityProfileEmailoremailLocalPartMatchingUserEntityName. Email-based resolvers are only safe if the provider verifies that the user controls the address. The documentation is explicit: use the minimum number of resolvers, ideally one, to avoid account hijacking. - Proxy providers:
awsAlb,cfAccessandgcpIapverify the signature of the identity the proxy passes on.oauth2Proxydoes not, so an internal user who can reach the backend directly can forge an identity. Restrict those endpoints or choose a verifying provider. See the Identity-Aware Proxy Guide. - Guest provider: makes all users share one guest identity. Useful for local development, never for production.
- Backstage token: an asymmetrically signed JWT carrying the user's entity reference and ownership claims. Signing keys rotate continuously and are unique per installation. Any plugin in the same backend, or sharing the auth plugin's database, can forge tokens, so high-security deployments should run
authas a separate service with its own database. See the JWT Security Guide. - Limited user token: the
uip(user identity proof) claim lets a token be reduced to a proof of identity that most endpoints reject. Plugins that accept limited tokens widen the attack surface, so avoid accepting them except for cases such as cookie auth of static assets.
What is not protected
Built-in protection against unauthenticated access only applies with the new backend system and without backend.auth.dangerouslyDisableDefaultAuthPolicy. Even then, the frontend bundle (all frontend plugin code, minified) is served to anyone who can reach the site unless you use the experimental public entry point, which builds a separate bundle for unauthenticated visitors.
Admin roles and privileged access
Backstage has no built-in administrator role. Privilege comes from the permission policy you write, the ability to change configuration, and control over the catalog's organisational data.
| Privilege | Where it comes from | Why it matters | Control |
|---|---|---|---|
| Policy author | The permission policy module (code) in the backend | Decides every authorisation result | Code owners and mandatory review on the policy module |
| Config author | app-config.yaml, app-config.production.yaml and secret injection | Can disable auth, add proxy routes with credentials, change reading allow lists | Separate repository or protected path, review by security |
| Org data source | The entity provider that ingests User and Group entities (for example from Entra ID or LDAP) | Group membership drives ownership and therefore permissions | Ingest only from a trusted provider; block user-registered User and Group entities |
| Template author | Template entities in the catalog | Templates run actions on backend hosts with the Scaffolder's credentials | Restrict who can register and change Template entities |
| Platform operator | Host, Kubernetes namespace and database access | Fully trusted by design; can read every secret | Treat as privileged access with session logging |
Turning on the permission framework
- Set
permission.enabled: truein app-config. - Remove
@backstage/plugin-permission-backend-module-allow-all-policyfrom the backend and add your own policy module (scaffolded withyarn new --select permission-policy-module), or a shared RBAC implementation. - Start with a policy that denies catalog entity deletion, location creation and Scaffolder template execution for everyone except owning groups, then widen by need.
- Test in a staging instance with real group data: sign in as a member of each key group and check catalog, TechDocs and Scaffolder behaviour. Rollback is redeploying the previous policy module version.
Permissions only protect endpoints whose plugins call the permission framework. Check each third-party plugin for permission support before relying on the policy; a plugin without it is open to every signed-in user. TechDocs visibility follows catalog read permissions, so restricting catalog reads also restricts documentation.
Catalog rules
By default the catalog only ingests Component, API and Location kinds from user-registered locations. Keep User, Group and Template out of catalog.rules for user-registered locations. The threat model warns that letting users register User and Group entities opens impersonation and confused group membership. Entities emitted directly by an entity provider are always trusted and are not subject to rules, so that provider's source is part of your identity trust chain.
Integrations, API tokens and service accounts
Backstage's value comes from integrations, and every integration is a non-human identity held by the backend.
| Credential | Configured in | Typical reach | Reduce it by |
|---|---|---|---|
| GitHub App (app ID, client ID, client secret, webhook secret, private key) | integrations.github, often via an included YAML file | Organisation repositories; CLI-created apps default to metadata:read and contents:read | Keep read-only; give the Scaffolder user OAuth for repository creation instead of write on the app |
| GitLab, Azure DevOps, Bitbucket tokens | integrations.* | Every repository the token's account can read | Dedicated bot accounts, narrow scopes, expiry |
| Auth provider client secrets | auth.providers.* (for example ${AUTH_GITHUB_CLIENT_SECRET}) | Ability to complete sign-in flows | One provider; secret in a vault |
| Proxy credentials | proxy.endpoints with injected headers | Anyone who can reach Backstage can call the upstream with those credentials | Replace with a backend plugin; if unavoidable use allowedMethods and minimal scope |
| Plugin tokens (CI, cloud, monitoring, incident tools) | Each plugin's config block | Varies; often admin API keys | Read-only keys per plugin; inventory owner and expiry |
| Static external access tokens | backend.auth.externalAccess type static | Unlimited without accessRestrictions | Restrict by plugin, permission and permissionAttribute |
| JWKS external access | externalAccess type jwks with url, issuer, algorithm, audience | Any workload that can get a token from that issuer | Pin issuer, algorithm and audience; add access restrictions |
| Plugin signing keys | Database by default, or backend.auth.pluginKeyStore static ES256 keys | Can mint plugin-to-plugin tokens | Prefer the default; for static keys rotate by adding a new key at the top of the array |
- Keep secrets out of YAML: app-config supports
$envand$filesubstitution; inject values from a secrets manager at deploy time, never commit them. Mark sensitive schema fields withvisibility: secret, and never mark a credentialfrontend, since frontend-visible config is shipped to the browser. See the Secrets Management Guide. - Prefer JWKS over static tokens for CI and automation that can obtain workload tokens, so there is no long-lived shared secret. Static tokens should be at least 24 random bytes, one per caller with a distinct
subjectso logs show who called. - Scaffolder identity: the Scaffolder often has elevated rights, such as creating repositories. The threat model recommends running actions with the user's own credentials where possible, so a template cannot do more than its user could.
- The reading allow list:
backend.reading.allowlists the hosts the backend may read on behalf of users. It runs with service context and ignores the permission system, so a$textplaceholder incatalog-info.yamlcan read a repository the user cannot. Keep the list to named source control hosts and never include metadata addresses such as 169.254.169.254: server-side reads of the metadata service were the route in the Capital One breach. See the SSRF Prevention Guide.
For managing these as accountable non-human identities, see the GitHub and GitHub Actions Security Guide and the API Key Management Guide.
Logging and detection
Backstage provides an Auditor core service for security-relevant events. It writes through the root logger by default, with events named by eventId (for example entity-fetch, entity-mutate, location-mutate), a meta object with fields such as actionType, entityRef and locationRef, and a success or failure outcome with actor details. Ship the backend's JSON logs to your SIEM and add the sources below.
| Source | Signal | Detection idea |
|---|---|---|
| Auditor events | location-mutate create, entity-mutate delete | Locations registered pointing to unknown hosts; bulk deletions |
| Scaffolder task logs | Template runs, actions, parameters | New or changed Template entities followed by runs; actions that delete resources |
| Auth backend logs and IdP sign-in logs | Sign-ins, resolver failures | Sign-in resolved to a different user than the IdP subject; spikes in resolver errors |
| Ingress or authenticating proxy | Requests and source addresses | Direct requests to backend ports bypassing the proxy; calls to /api/proxy paths from unusual users |
| Integration providers (GitHub audit log, cloud trails) | Actions by the Backstage App or bot tokens | Backstage credentials used from addresses other than the Backstage egress |
| Dependency scanning | Advisories against yarn.lock | New critical advisory in a Backstage or plugin package |
Relevant ATT&CK techniques are T1195.002 Compromise Software Supply Chain (malicious plugin or dependency), T1552.005 Cloud Instance Metadata API (abuse of the reading allow list), T1550.001 Application Access Token (reuse of integration tokens) and T1199 Trusted Relationship.
Metrics
- Policy coverage: share of installed backend plugins that enforce permissions.
- Unrestricted external access entries:
externalAccessentries withoutaccessRestrictions(target 0). - Credential inventory: integration and plugin credentials with a named owner and expiry date (target 100 percent).
- Write-capable integrations: count of integration credentials with write scope.
- Dependency lag: days between a Backstage release with a security fix and your deployment.
Hardening baseline
- Network: no public exposure. Front Backstage with an authenticating proxy (AWS ALB with OIDC, Google IAP or Cloudflare Access) and the matching verifying provider. Backstage offers only rudimentary denial of service protection, so rate limit at the proxy.
- Backend auth: run the new backend system on a supported release, confirm
dangerouslyDisableDefaultAuthPolicyis not set, and use the public entry point if the frontend bundle itself is sensitive. - Sign-in: one provider, one resolver, no guest in production, MFA at the identity provider.
- Authorisation: enable permissions, replace the allow-all policy, and restrict catalog mutations and Scaffolder execution.
- Catalog: organisational data only from a trusted entity provider; catalog rules that exclude User, Group and Template from user locations; disable or replace placeholder resolvers if
$textreads are a concern. - Secrets: all credentials injected from a vault, read-only scopes, inventory with owners, rotation dates.
- Isolation: split sensitive plugins (auth, Scaffolder, plugins with admin keys) into separate backend deployments with separate databases and credentials. Shared databases mean shared trust.
- Supply chain: vet every plugin and Scaffolder action before installing, pin versions, run Dependabot or Renovate on
yarn.lock, and follow the project's GitHub Security Advisories. See the npm Install-Time Security Guide.
Small team or large platform
- Small deployments: a single backend is acceptable if it is behind an authenticating proxy, uses read-only integration credentials, and has the permission framework on. Avoid installing plugins that need admin API keys.
- Large platforms: separate auth and Scaffolder services, per-plugin databases, JWKS-based external access, and policy as code with tests in CI. Treat the Backstage platform team's deploy pipeline as privileged.
Common misconfigurations
- Allow-all policy left in place: teams enable SSO and assume that is access control. Every signed-in user can delete catalog entities and run every template.
- Unverified proxy identity:
oauth2Proxywith the backend reachable directly, so a header supplied by an internal user is trusted as identity. - Several loose resolvers: an email local-part resolver plus a username resolver lets
[email protected]sign in as the employee User entityjsmith. - Credentials in the proxy plugin: an admin API key for a monitoring tool injected as an
Authorizationheader, usable by anyone who can reach Backstage. - Write-scoped GitHub App for the Scaffolder: any template, or any user who can author one, can create, change or delete repositories across the organisation.
- Wide reading allow list: wildcard hosts in
backend.reading.allowthat let the backend read internal services or metadata endpoints on a user's behalf.
Framework mapping
| Control area | NIST SP 800-53 Rev 5 | NIST CSF 2.0 | CIS Controls v8.1 | ISO/IEC 27001:2022 Annex A |
|---|---|---|---|---|
| Permission policy and least privilege | AC-3, AC-6 | PR.AA-05 | 6.8 | A.5.15, A.8.3 |
| Integration and external access credentials | IA-5, IA-9 | PR.AA-01 | 5.2, 3.11 | A.5.17 |
| Network isolation and reading allow list | SC-7, CM-7 | PR.PS-01 | 4.4, 12.2 | A.8.20, A.8.22 |
| Plugin and dependency vetting | SR-3 | PR.PS-01 | 16.4 | A.5.21, A.8.28 |
| Audit events | AU-2 | DE.CM-09 | 8.2 | A.8.15 |
Practitioner checklist
- Confirm Backstage is unreachable from the internet and fronted by a signature-verifying authenticating proxy.
- Check that
backend.auth.dangerouslyDisableDefaultAuthPolicyis absent and the new backend system is in use. - Reduce sign-in to one provider and one resolver; remove guest sign-in from production config.
- Set
permission.enabled: true, remove the allow-all policy module, and test the policy per group. - Exclude User, Group and Template kinds from user-registered locations in
catalog.rules. - Inventory every integration and plugin credential with owner, scope and expiry; make them read-only where possible.
- Add
accessRestrictionsto everyexternalAccessentry, and prefer JWKS over static tokens. - Remove credentials injected through proxy endpoints, or restrict them with
allowedMethodsand minimal scope. - Restrict
backend.reading.allowto named source control hosts. - Forward Auditor events and Scaffolder logs to the SIEM and track plugin and dependency advisories.
Standards and references
- Backstage: Security and Threat Model
- Backstage: Service to Service Auth
- Backstage: Sign-in Identities and Resolvers
- Backstage: Permission Framework Overview
- Backstage: GitHub Apps for Backend Authentication
- Backstage: Auditor Service
- Backstage GitHub: Security Policy and Advisories
- MITRE ATT&CK: T1195.002 Compromise Software Supply Chain
Related NHI Mgmt Group resources: GitHub and GitHub Actions Security Guide · CI/CD Pipeline Identity Security Guide · Secrets Management Guide · DevSecOps Guide