diff --git a/docs/auth/service-to-service-auth.md b/docs/auth/service-to-service-auth.md new file mode 100644 index 0000000000..7c501a8e9e --- /dev/null +++ b/docs/auth/service-to-service-auth.md @@ -0,0 +1,164 @@ +--- +id: service-to-service-auth +title: Service to Service Auth +# prettier-ignore +description: This section describes how to use service to service authentication, both internally within Backstage plugins and towards external services. +--- + +This article describes the steps needed to introduce _backend-to-backend auth_. +This allows plugin backends to determine whether a given request originates from +a legitimate Backstage plugin (or other external caller), by requiring a special +type of service-to-service token which is signed with a shared secret. + +When enabling this protection on your Backstage backend plugins, for example the +catalog, other callers in the ecosystem such as the search indexer and +scaffolder would need to present a valid token to the catalog to be able to +request its contents. + +## Setup + +In a newly created Backstage app, the backend is setup up to not require any +auth at all. This means that generated service-to-service tokens are empty, and +that incoming requests are not validated. If you want to enable +service-to-service auth, the first step is to switch out the following line in +your backend setup at `packages/backend/src/index.ts`: + +```diff +- const tokenManager = ServerTokenManager.noop(); ++ const tokenManager = ServerTokenManager.fromConfig(config, { logger: root }); +``` + +By switching from the no-op `ServiceTokenManager` to one created from config, +you enable service-to-service auth for any plugin that implements it. The local +development setup will generally not be impacted by this, as temporary keys are +generated under the hood. But for the production setup, this means you must now +provide a shared secret that enables your backend plugins to communicate with +each other. + +Backstage service-to-service tokens are currently always signed with a single +secret key. It needs to be shared across all backend plugins and services that +ones wishes to communicate across. The key can be any base64 encoded secret. +The following command can be used to generate such a key in a terminal: + +```bash +node -p 'require("crypto").randomBytes(24).toString("base64")' +``` + +Then place it in the backend configuration, either as a direct value or +injected as an env variable. + +```yaml +# commonly in your app-config.production.yaml +backend: + auth: + keys: + - secret: + # - secret: ${BACKEND_SECRET} - if you want to use an env variable instead +``` + +**NOTE**: For ease of development, we auto-generate a key for you if you haven't +configured a secret in dev mode. You _must set your own secret_ in order for +backend-to-backend auth to work in production; the `ServiceTokenManager` will +throw an exception in production if it has no keys to work with, which will lead +to the backend failing to start up. + +## Usage in Backend Plugins + +There are a few steps if you want to make use of the service-to-service auth in +your own backend plugin. First you need to add the `TokenManager` dependency to +the `createRouter` options. Typically as `tokenManager: TokenManager`. Along +with this you'll need to ask users to start providing this new dependency in +their backend setup code. + +Once the `TokenManager` is available, you use the `.getToken()` method to generate +a new token for any outgoing requests towards other Backstage backend plugins. +This method should be called for every request that you make; do not store the +token for later use. The `TokenManager` implementations should already cache +tokens as needed. The returned token should then be added as a `Bearer` token +for the upstream request, for example: + +```ts +const { token } = await this.tokenManager.getToken(); + +const response = await fetch(pluginBackendApiUrl, { + method: 'GET', + headers: { + ...headers, + Authorization: `Bearer ${token}`, + }, +}); +``` + +To authenticate an incoming request you use the `.authenticate(token)` method. +At the time of writing this method doesn't return anything, it will simply +throw if the token is invalid. + +```ts +await tokenManager.authenticate(token); // throws if token is invalid +``` + +## Usage in External Callers + +If you have enabled server-to-server auth, you may be interested in generating +tokens in code that is external to Backstage itself. External callers may even +be written in other languages than Node.js. This section explains how to generate +a valid token yourself. + +The token must be a JWT with a `HS256` signature, using the raw base64 decoded +value of the configured key as the secret. It must also have the following payload: + +- `sub`: "backstage-server" (only this value supported currently) +- `exp`: one hour from the time it was generated, in epoch seconds + +## Granular Access Control + +We plan to build out the service-to-service auth to be much more powerful in the +future, but before that is done there are a few tricks you can use with the +current system to harden your deployments. This section assumes that you have +already split your backend plugins into more than one backend deployment, in +order to scale or isolate them. + +The backend auth configuration has support for providing multiple keys, for +example: + +```yaml +backend: + auth: + keys: + - secret: my-secret-key-1 + - secret: my-secret-key-2 + - secret: my-secret-key-3 +``` + +The first key will be used for signing requests, while all of the keys will be +used for validation. This means that you can set up an asymmetric configuration +where some backend deployments do not have access to each other. + +For example, consider the case where we have split up the catalog, scaffolder, +and search plugin into three separate backend deployments. We can use the +following configurations to allow both the scaffolder and search plugin to speak +to the +catalog, but not the other way around, and to not allow any communication between +the scaffolder and search plugins. + +```yaml +# catalog config +backend: + auth: + keys: + - secret: my-secret-key-catalog + - secret: my-secret-key-scaffolder + - secret: my-secret-key-search + +# scaffolder config +backend: + auth: + keys: + - secret: my-secret-key-scaffolder + +# search config +backend: + auth: + keys: + - secret: my-secret-key-search +``` diff --git a/docs/tutorials/backend-to-backend-auth.md b/docs/tutorials/backend-to-backend-auth.md index 7de751be77..607356927c 100644 --- a/docs/tutorials/backend-to-backend-auth.md +++ b/docs/tutorials/backend-to-backend-auth.md @@ -4,68 +4,4 @@ title: Backend-to-Backend Authentication description: Guide for authenticating API requests between Backstage plugin backends --- -This tutorial describes the steps needed to handle _backend-to-backend -authentication_, which allows plugin backends to determine whether a given -request originates from a legitimate Backstage backend by verifying a token -signed with a shared secret. This system has limited use for now, but will be -needed to support the upcoming framework for permissions and authorization (see -[the PRFC on the topic](https://github.com/backstage/backstage/pull/7761) for -more details). - -Backends have no concept of a Backstage identity, so instead they use a token -generated using a shared key stored in config. You can generate a unique key for -your app in a terminal, and set the `BACKEND_SECRET` environment variable to the -resulting value. - -```bash -node -p 'require("crypto").randomBytes(24).toString("base64")' -``` - -**NOTE**: For ease of development, we auto-generate a key for you if you haven't -configured a secret in dev mode. You _must set your own secret_ in order for -backend-to-backend authentication to work in production. - -Requests originating from a backend plugin can be authenticated by decorating -them with a backend token. Backend tokens can be generated using a -`TokenManager`, which can be passed to plugin backends via the -`PluginEnvironment`. The `TokenManager` provided in new Backstage instances -generated by `create-app` is a stub, which returns empty tokens and accepts any -input string as valid. To enable backend-to-backend authentication, you'll need -to instantiate a new one using the secret from your config instead: - -```diff -// packages/backend/src/index.ts - -function makeCreateEnv(config: Config) { - const root = getRootLogger(); - const reader = UrlReaders.default({ logger: root, config }); - const discovery = SingleHostDiscovery.fromConfig(config); - - root.info(`Created UrlReader ${reader}`); - - const cacheManager = CacheManager.fromConfig(config); - const databaseManager = DatabaseManager.fromConfig(config); -- const tokenManager = ServerTokenManager.noop(); -+ const tokenManager = ServerTokenManager.fromConfig(config, { logger: root }); -``` - -With this `tokenManager`, you can then generate a server token for requests: - -```typescript -const { token } = await this.tokenManager.getToken(); - -const response = await fetch(pluginBackendApiUrl, { - method: 'GET', - headers: { - ...headers, - Authorization: `Bearer ${token}`, - }, -}); -``` - -You can use the same `tokenManager` to authenticate tokens supplied on incoming -requests: - -```typescript -await tokenManager.authenticate(token); // throws if token is invalid -``` +See [new docs](../auth/service-to-service-auth.md) diff --git a/microsite/sidebars.json b/microsite/sidebars.json index f00b7b2ee1..fbad2529e5 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -279,6 +279,7 @@ "auth/identity-resolver", "auth/oauth", "auth/add-auth-provider", + "auth/service-to-service-auth", "auth/troubleshooting", "auth/glossary" ], @@ -335,7 +336,6 @@ "tutorials/configuring-plugin-databases", "tutorials/switching-sqlite-postgres", "tutorials/using-backstage-proxy-within-plugin", - "tutorials/backend-to-backend-auth", "tutorials/yarn-migration" ], "Architecture Decision Records (ADRs)": [ diff --git a/mkdocs.yml b/mkdocs.yml index 8c86ae095d..9612a68534 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -164,6 +164,7 @@ nav: - Sign in resolvers: 'auth/identity-resolver.md' - OAuth and OpenID Connect: 'auth/oauth.md' - Contributing New Providers: 'auth/add-auth-provider.md' + - Service to Service Auth: 'auth/service-to-service-auth.md' - Troubleshooting Auth: 'auth/troubleshooting.md' - Glossary: 'auth/glossary.md' - Deployment: