Integrate Vault with Backstage and Okta
Authors: Mustafa EL-Hilo and Chris Zembower
This guide details how to configure and integrate Backstage with Vault by using Okta as a source of identity for authentication. This integration enables least-privilege access between the systems.
Organizations use Backstage as an internal developer platform (IDP) to improve developer experience and accelerate delivery by providing a standardized solution to integrate with other systems. You can use Backstage to let developers manage Vault resources from Backstage’s UI. For example, you can expose a Vault KV secrets engine in Backstage so a developer can modify the values in the Backstage UI.
You should use JWT-based authentication when authenticating to Vault to enable granular, user-specific access control. JWT-based authentication ensures that each user receives a unique, short-lived token that Vault can verify. By eliminating long-lived credentials and tying authentication to an external identity provider, you simplify identity management and provide an audit trail of user interactions with Vault. While this guide focuses on implementing JWT authentication using Okta, you can apply the same pattern to other identity providers that support OIDC.
When you integrate Vault, Backstage, and Okta you gain the following benefits:
- Standardize: Have consistent and repeatable interactions to Vault
- Use principle of least privilege: Grant the minimum access rights necessary to perform tasks
- Consistent access: Extending security control from Backstage to Vault gives users the same authorization in both systems
- Eliminate static credentials: Vault delivers tokens just-in-time and expires after 60 minutes
- Simplify credential management: Store secrets in a centralized management system
- End-to-end audibility: Ability to audit the entire secrets management stack
Target audience
This guide references the following roles:
- Platform operator: Someone who manages your organization's platform. This may include security and platform teams. This is an all-encompassing role that may include the responsibilities of the Vault administrator and producer. For this specific guide, the platform team is also responsible for managing and configuring Backstage.
- Vault consumer: Someone who has access to read Vault secrets, such as application and DevOps engineers.
Prerequisites
To follow this guide, the following roles will need the following permissions. These roles are part of the platform operator.
- Vault administrator and producer:
- Privileged access to a Vault Enterprise or HCP Vault Dedicated cluster that includes permission to create auth mounts, configure auth roles, and create policies
- Basic familiarity with OpenID Connect (OIDC), specifically JWT claims and audiences
- HCP Terraform workspace from which to manage Vault configurations
- Okta administrators:
- Access and permissions to create an Okta application with defined user groups
- Understanding of Okta groups
- Backstage platform team:
- Ability to modify Backstage Plugins, specifically:
- Vault - modify plugin to leverage a JWT for authentication
- Okta - modify claims and audiences to establish relationship with Vault
- Ability to modify Backstage Plugins, specifically:
To follow this guide, you will need the following.
- A local development Vault environment
- A local development Backstage environment
- A development Okta environment or a new application
- Note the
clientId
,clientSecret
andaudience
values
- Note the
- Backstage - v1.31.1 with node v20.17.0 (npm v10.8.2)
- Vault - v1.18.0 with the Terraform Vault Provider - v4.6.0
- A configured Okta Application that Backstage can use. After creating the application, you will have the
clientId
,clientSecret
andaudience
values that you can use to configure Backstage.
You will need to know the following concepts and actions. The following resources contains information to guide you through these steps.
- Vault:
- How to configure a JWT/OIDC provider using the user's web browser
- Start and prepare Vault
- How to use the /sys/config/cors endpoint to configure CORS
- Construct a Vault cURL command
- Backstage:
- Okta Authentication Provider to authenticate users using Okta OpenID Connect
- Creating your Backstage App
- Sign-in Identities and Resolvers
- Configure your own public and private key files to sign issued tokens
Limitations
Backstage and Vault are highly configurable and extensible. This guide provides the steps to authenticate between the systems using a JWT and not a fully functional Backstage plugin.
Background and best practices
This section contains best practices for implementing Vault integration for Backstage and Okta. You will need to understand these concepts and best practices to securely and efficiently manage secrets.
Vault's JWT auth method is highly flexible, letting you authorize based on a range of token parameters. You can scope auth roles depending on the nature and sensitivity of the secrets being accessed. As you design your solution, consider the data passed from Okta to Backstage to Vault to enable the creation of granular policy definitions.
Ideally, you can use a single Vault JWT auth role with a templated policy to dynamically grant access to a secret. In this example, Vault uses the value of sub
to grant access to a specific KV path. This particular approach requires that you create KVs paths based on team names.
# Vault policy to read KV secrets for a specific team
path "kv/data/{{identity.entity.aliases.auth_jwt_.metadata.sub}}/*" {
capabilities = ["read", "list"]
}
When granting access and creating resources in Vault, we recommend you combine policy templating with well-defined patterns to simplify operations and enable rapid scaling. Without these patterns, maintaining relationships manually would be operationally intensive.
It is important to design your Vault resource management processes using a pattern-based path structure. This structure should align with your organizational hierarchy, as defined in Okta or another system that Backstage could integrate with. Finding a pattern to match on will significantly simplify the implementation, some possible patterns can be application IDs, user groups or business units.
People and process considerations
When security teams adopt Vault to centralize secrets management, they simplify audit and governance processes and empower application teams to consume sensitive data in alignment with DevOps practices, meeting increasingly stringent compliance requirements. By extending Vault access to Backstage, developers can leverage a wide variety of secrets engines and capabilities while adhering to organizational policy and supporting business priorities in the realm of data protection. When you integrate with Vault, you integrate security into the tools developers already use, reducing friction and encouraging adoption.
Once a user authenticates to Vault via Backstage, Vault assigns a user an identity. The identity lets the Vault team track and audit that user’s interactions with Vault, ensuring accountability and providing detailed insights into user and client behaviors related to secret access. This visibility helps the Vault team enforce security policies, identify potential misuse, and optimize configurations to support organizational needs.
If you need to provide developers with direct access to Vault for the same actions they will perform through Backstage, you can configure Vault external groups along with aliases. Aliases ensure that whether a developer authenticates with Vault or Backstage, they are treated as a single client, maintaining consistent privileges and efficient utilization of your product entitlements.
Validated architecture
The validated architecture shows a high-level overview of how to integrate Backstage, Vault, and Okta. Each of these components play a role in the authentication workflow.
Connect Okta to Backstage: This involves configuring a custom resolver, which lets you modify JWT claims to include additional data that you can pass to Vault and custom business logic. For example, you can pass user group names and mapping application numbers to teams.
Configure Vault: Once you configure Backstage and Okta, you will create a Vault JWT authentication method, role and policy. While we recommend that you manage all Vault resources with Terraform, you can perform these steps with the CLI or API. You only need to configure the authentication method once.
While designing your solution, consider the mapping between the Backstage token claims and the Vault JWT auth method. This mapping has an impact on how Vault interprets templated policies.
For example, if you want to restrict access based on a user's group, you should.
- Ensure the Backstage JWT includes the user's group as a claim.
- Configure the Vault JWT authentication role to map the group claim from the token.
- Define a Vault policy that grants access based on the mapped group claim.
Because the Backstage frontend authenticates to Vault, you will need to modify the settings for cross-origin requests (CORS). Since HCP Vault Dedicated customers cannot modify the CORS settings directly, please reach out to the HashiCorp support team to assist you with this task.
You should configure the Vault resources that developers will interact with, such as a KV secrets engine, during an onboarding process. As a best practice, we recommend you manage Vault resources with Terraform. This enables automation, ensuring that Vault roles, policies, and secret paths are consistently provisioned and maintained. By automating the Vault resource creation, you can use Backstage or another system to enable self-service onboarding for new teams, reducing operational overhead.
Encoded JWTs can represent a security risk under certain circumstances, particularly if you have configured Vault to authorize access for a given OIDC provider’s tokens. We do not recommend exposing encoded tokens outside of your security boundaries. Decoded tokens, while not secrets, may additionally be deemed sensitive by your organization due to the descriptive nature of the claims.
Develop Backstage Vault frontend: By configuring Backstage, Okta, and Vault, the underlying authentication workflow should be functional. You are ready to develop a Backstage frontend plugin. This plugin will use a JWT to authenticate to Vault and execute a development workflow such as modifying a Vault KV secret.
Checklist
Make sure you have the following in place:
- Access to a Vault development environment
- Access to a Backstage development environment
- Access to Okta to create an application following these steps
We recommend you complete these steps on non-production infrastructure before you configure your production environments.
Configure Backstage with a public and private key
By default, the Backstage authentication backend generates and manages its own signing keys automatically for any issued Backstage tokens. However, these keys have a short lifetime and do not persist after instance restarts.
The Backstage platform engineer needs to create a public/private key pair. Vault will use this public key to validate the JWT.
Note
For production environments, use your organization's existing TLS certificate issuance service rather than manually generating certificates. The following steps are intended for local development only.
Generate a private key using the ES256 algorithm, convert the key to PKCS#8 format, and extract the public key.
$ openssl ecparam -name prime256v1 -genkey -out private.ec.key &&
openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in private.ec.key -out private.key &&
openssl ec -inform PEM -outform PEM -pubout -in private.key -out public.key
Add the public and private key path to the app-config.yaml
file for Backstage.
app-config.yaml
auth:
keyStore:
provider: static
static:
keys:
# Must be declared at least once and the first one will be used for signing
- keyId: primary
publicKeyFile: /path/to/public.key
privateKeyFile: /path/to/private.key
Configure Backstage to use Okta
Depending on your Okta configuration and the metadata you want from Okta, modify additionalScopes
. For example, you can add more configurations such as authServerId
and idp
. Notice the missing resolvers
stanza. This will ensure Backstage will automatically use the custom resolver.
The Backstage platform engineer should add the Okta details to the app-config.yaml
file under the auth
stanza.
app-config.yaml
auth:
keyStore:
provider: static
static:
keys:
- keyId: primary
publicKeyFile: /path/to/public.key
privateKeyFile: /path/to/private.key
environment: development
providers:
guest: {}
okta:
development:
clientId: '${AUTH_OKTA_CLIENT_ID}'
clientSecret: '${AUTH_OKTA_CLIENT_SECRET}'
audience: '${AUTH_OKTA_DOMAIN}'
additionalScopes:
- groups
- openid
- email
- profile
Configure Backstage custom resolver
Custom resolvers let you specify the data you need in the JWT payload. The following example adds logging to help you troubleshoot. You should remove logging prior to going to production since this would output the JWT to log.
You can decode the token printed using freely-available tools such as jwt.io. You can validate the signature using the public key, but this is not required to view the parsed token.
To use a custom resolver, the Backstage platform engineer creates a new file at packages/backend/src/plugins/CustomOktaAuth.ts
.
CustomOktaAuth.ts
import { createRouter } from '@backstage/plugin-auth-backend';
import { createBackendModule } from '@backstage/backend-plugin-api';
import { oktaAuthenticator } from '@backstage/plugin-auth-backend-module-okta-provider';
import {
authProvidersExtensionPoint,
createOAuthProviderFactory,
} from '@backstage/plugin-auth-node';
import { stringifyEntityRef, DEFAULT_NAMESPACE } from '@backstage/catalog-model';
export const customOktaAuth = createBackendModule({
// This ID must be exactly "auth" because that's the plugin it targets
pluginId: 'auth',
moduleId: 'custom-auth-provider',
register(reg) {
reg.registerInit({
deps: { providers: authProvidersExtensionPoint },
async init({ providers }) {
providers.registerProvider({
providerId: 'okta',
factory: createOAuthProviderFactory({
authenticator: oktaAuthenticator,
async signInResolver(info, ctx) {
const { profile: { email } } = info;
if (!email) {
throw new Error(
'Login failed, user profile does not contain an email',
);
}
let fullProfileJson = (info.result.fullProfile as any)._json;
const openid = fullProfileJson.sub;
console.log(info);
console.log(fullProfileJson);
console.log(openid);
let departmentName = "";
if (fullProfileJson.groups) {
departmentName = fullProfileJson.groups[0];
}
const userEntity = stringifyEntityRef({
kind: 'User',
name: openid,
namespace: DEFAULT_NAMESPACE,
});
return ctx.issueToken({
claims: {
sub: userEntity,
openid: openid,
ent: ["group:" + departmentName],
email: email,
},
});
},
}),
});
},
});
},
});
Add to packages/backend/src/index.ts
under auth plugin
.
backend.add(customOktaAuth);
Add to packages/app/src/App.tsx
.
import { oktaAuthApiRef } from '@backstage/core-plugin-api';
To enable Okta login page, add in components
.
SignInPage: props => (
<SignInPage
{...props}
providers={[
{
id: 'okta-auth-provider',
title: 'okta',
message: 'Sign in using okta',
apiRef: oktaAuthApiRef,
},
]}
/>
),
Validate authentication
When you log into Backstaging using Okta, Backstage will print the JWT.
The following is an example Backstage log that contains a JWT.You should be able to authenticate into Backstage using your Okta login.
[backend]: idToken: 'eyJraWQiOiJOSXVYaU81dkV4LT...'
The JWT is the full payload from Okta. The custom resolver can manipulate it to either add or remove metadata from the payload. Backstage will use the modified JWT to authenticate to Vault. The following is the decoded JWT using jwt.io.
{
"sub": "00ulsmoa58OzykeuD5d7",
"name": "Your Okta Name",
"email": "Your email",
"ver": 1,
"iss": "https://dev-58667473.okta.com",
"aud": "Your aud value",
"iat": 1737398903,
"exp": 1737402503,
"jti": "ID.B29Nyw9bmHiMsd93wLSf_sT74OJxnkaJG6vl2FvPByM",
"amr": [
"mfa",
"otp",
"pwd"
],
"idp": "00olsmo9zpn6P3ZoI5d7",
"preferred_username": "Your email",
"auth_time": 1737395107,
"at_hash": "qjYn24UU81QSB3GVnY8_EA",
"groups": [
"backstage-admin"
]
}
Configure Vault
After you configure Okta and Backstage, a Vault platform engineer will integrate Backstage with Vault to provide the JWT authentication method. A single JWT auth mount resource may service multiple underlying roles. You only need to configure the Vault JWT auth backend once per Vault namespace.
# Configure the JWT authentication method
resource "vault_jwt_auth_backend" "jwt_config" {
description = "JWT Auth Backend for Backstage"
path = "jwt"
bound_issuer = "http://localhost:7007/api/auth"
jwt_validation_pubkeys = [file("../ssl/public.key")]
}
The Vault platform engineer should create the appropriate Vault policies for the environment, following the principle of least privilege to limit client access to secrets.
The following example policy grants read access to all secrets under the path kv/app1
resource "vault_policy" "app1_workflow1" {
name = "backstage-read"
policy = <<EOT
path "kv/data/app1/*" {
capabilities = ["read"]
}
EOT
}
The Vault platform engineer configures a JWT authentication role in Vault that maps Backstage users to appropriate Vault policies. This task requires planning how teams or applications connect between the systems, using JWT metadata like audience, subject, and email claims.
The following example configures a role with a backstage
audience that verifies user identity and maps their email, linking them to the app1_workflow1
policy which controls permitted actions for the Backstage service.
resource "vault_jwt_auth_backend_role" "vault_role_okta_group_admins" {
backend = vault_jwt_auth_backend.jwt_config.path
role_name = "vault-role-okta-group-vault-admins"
role_type = "jwt"
user_claim = "sub"
bound_audiences = ["backstage"]
token_policies = [vault_policy.app1_workflow1.name]
claim_mappings = {
"email" = "email"
}
}
Backstage Vault frontend
You now validate the authentication workflow. The following example creates an example page in Backstage that will show that the authentication is successful. You will need to expand on this by building a Backstage frontend plugin for the features you want to expose from Vault to the Backstage user.
The Backstage platform engineer will apply CORS configurations to Vault so the Backstage frontend can interact with Vault without being blocked by cross-origin restrictions.
The Backstage engineer uses the Vault API token to configure a payload with the necessary CORS settings. You can only apply these settings for the root
namespace. For HCP Vault Dedicated, you will need to work with the support team to modify these values.
The following example add CORS values using a JSON payload.
$ curl -k --header "X-Vault-Token: <token/auth>" --request POST --data @cors_payload.json https://127.0.0.1:8200/v1/sys/config/cors
The content for cors_payload.json
.
{
"allowed_origins": "*"
}
Warning
Do not use *
in production as it's too permissive. Replace it with the value of Backstage's hostname.
The Backstage platform engineer will create a helper component, TokenCard.tsx
, which will fetch the JWT token from Okta using the identityApi
and then make a request to Vault’s JWT authentication endpoint to log in and retrieve a response. The component will display the Okta token and the Vault login response. This component is only for demonstration and troubleshooting purposes.
Create a new file packages/app/src/components/token/TokenCard.tsx
.
TokenCard.tsx
import React, { useEffect, useState } from 'react';
import { Card, CardContent, Typography, CircularProgress } from '@material-ui/core';
import { useApi, identityApiRef } from '@backstage/core-plugin-api';
const TokenCard: React.FC = () => {
const identityApi = useApi(identityApiRef);
const [token, setToken] = useState<string | null>(null); // State to hold the Okta token
const [vaultResponse, setVaultResponse] = useState<string | null>(null); // State to hold the Vault response
const [loading, setLoading] = useState<boolean>(true); // Loading state
useEffect(() => {
const fetchTokenAndLoginToVault = async () => {
try {
// Fetch the JWT token from Okta
const response = await identityApi.getCredentials();
const fetchedToken = response.token || null;
setToken(fetchedToken);
console.log("Fetched JWT Token:", fetchedToken);
if (fetchedToken) {
// API call to Vault
const requestUrl = 'https://127.0.0.1:8200/v1/auth/jwt/login';
const requestBody = JSON.stringify({
role: 'vault-role-okta-group-vault-admins',
jwt: fetchedToken,
});
const requestHeaders = {
'Content-Type': 'application/json',
// If using Vault namepsaces, add it to the header
// 'X-Vault-Namespace': 'admin',
};
// Log the details
console.log('Request URL:', requestUrl);
console.log('Request Headers:', requestHeaders);
console.log('Request Body:', requestBody);
const vaultResponse = await fetch(requestUrl, {
method: 'POST',
headers: requestHeaders,
body: requestBody,
});
console.log('Response Details:', vaultResponse);
const vaultData = await vaultResponse.json();
setVaultResponse(JSON.stringify(vaultData, null, 2));
} else {
setVaultResponse('No JWT token available for Vault login');
}
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
console.error('Error during Vault login:', errorMessage);
setVaultResponse(`Error: ${errorMessage}`);
} finally {
setLoading(false);
}
};
fetchTokenAndLoginToVault();
}, [identityApi]);
return (
<Card>
<CardContent>
<Typography variant="h6">Okta Token</Typography>
<Typography variant="body1">{token || 'No token available'}</Typography>
<Typography variant="h6" style={{ marginTop: '1rem' }}>Vault Login Response</Typography>
{loading ? (
<CircularProgress size={24} />
) : (
<Typography
variant="body2"
component="pre"
style={{
backgroundColor: '#181818',
padding: '1rem',
borderRadius: '4px',
overflowX: 'auto',
whiteSpace: 'pre-wrap',
}}
>
{vaultResponse || 'No response from Vault'}
</Typography>
)}
</CardContent>
</Card>
);
};
export default TokenCard;
Then, add a route for the page in packages/app/src/App.tsx
.
App.tsx
<Route path="/token" element={<TokenCard />} />
You will see a JWT and a response from Vault when you go to http://localhost:3000/token.
As an additional troubleshooting step, use the JWT from this page and manually log into Vault using the CLI.
$ vault write auth/jwt/login role="vault-role-okta-group-vault-admins" jwt="YOUR TOKEN" -format=json
Conclusion
In this guide, you learned how to integrate Backstage and Okta into Vault using JWT authentication. THis integration creates a secure and efficient authentication workflow that uses JWT-based authentication to manage secrets in Vault.
To learn more, check out the following resources:
- Manage Vault policies using Terraform
- Use JWT/OIDC authentication
- OIDC authentication with Okta
- OAuth and OpenID Connect in Backstage Software Catalog and Developer Platform