From 8d49e5a62c66ef0532fa991b72235c8b82df05f5 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 15 May 2023 13:40:43 +0200 Subject: [PATCH 01/11] CI Enabling Upload Tokens --- text/0091-ci-upload-tokens.md | 173 ++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 text/0091-ci-upload-tokens.md diff --git a/text/0091-ci-upload-tokens.md b/text/0091-ci-upload-tokens.md new file mode 100644 index 00000000..5f8696aa --- /dev/null +++ b/text/0091-ci-upload-tokens.md @@ -0,0 +1,173 @@ +- Start Date: 2023-05-15 +- RFC Type: feature +- RFC PR: +- RFC Status: draft + +# Summary + +This RFC Proposes an improved CI experience for uploading source maps, debug symbols, +and potentially other CI based operations by proposing a new way to get and manage +access tokens specifically for this environment. + +# Motivation + +Today there are two ways to get access tokens for use with sentry-cli: + +1. per user access tokens +2. internal organization integrations + +Either one are not great. The per user token is easy to get access to (which is why +they are preferred in the docs still) but they run into the risk that a user departs +an organization and an integration stops working. The organization integration flow +is complex and requires elevated privileges. Either of those options have the additional +complexity that there are a lot of extra settings to get right when configuring the tools. +For instance the token itself does not know where it goes to, which requires organization +slug and project slug to be set. All of this together means that the documentation does +not put a user on the path of success but requires multiple separate steps to get +everything in order. + +# Background + +We improved a lot of the inner workings of source maps and debug files at Sentry recently +but these efforts are held back by the complexity of getting the token. The friction is +still too high for many customers to make the necessary investment into getting source maps +uploaded. From a documentation writing and onboarding experience, it's also not clear with +the current system how the experience can be improved. + +# Technical Implementation + +The motivation is to add a new kind of token to Sentry which are fundamentally per-organization +tokens, but with the ability to carry meta information that tools like sentry-cli can use to +improve the user experience. These org level tokens can be created by anyone in the org, they +can be given additional restrictions, and they can carry meta information such as routing +data. For the purpose of this document they are called **structural tokens**. + +## Token Format + +The proposed token format is undecided so far. The goals of the token align generally with +both [Macaroons](http://macaroons.io/) and [Biscuit](https://www.biscuitsec.org). Unfortunately +the former standard has never seen much attention, and the latter is pretty new and not +particularly proven. Either system however permits adding additional restrictions to the +token which make them a perfect choice for the use in our pipeline. Biscuits however seem +quite promising. The idea of biscuits is that the token itself holds _facts_ which and +can be further constraints via _checks_ and they are checked against a datalog inspired +_policies_. + +One of the benefits of having the tokens carry this data is that the token alone has enough +information available to route to a Sentry installation. This means that `sentry-cli` or +any other tool _just_ needs the token to even determine the host that the token should be +sent against. + +The token then gets a prefix `sntrys_` (sentry structure) to make it possible to +detect it by security scrapers. Anyone handling such a token is required to check +for the `sntrys_` prefix and disregard it before parsing it. + +It's unclear at the moment if Biscuit is a good choice. There is an alternative where +instead of expressing everything in Biscuit tokens, the token gains a base64 encoded +JSON payload that the client can parse containing just the facts. + +## Token Facts + +Tokens in Biscuit contain facts. Each fact can later be referenced by the policy and +further restrictions can be placed on the token. For instance this is a basic set of +token facts for a token generated in the UI that has permissions to releases and +org read. + +```javascript +site("https://myorg.sentry.io"); +org("myorg"); +project("myproject"); +scope("project:releases"); +scope("org:read"); +``` + +Alternatively we we decide not to consider Biscuit the facts can be encoded into +a JSON structure. Note that in this case we would not transmit the scopes. + +```json +{ + "site": "https://myorg.sentry.io", + "org": "myorg", + "projects": ["myproject"] +} +``` + +Encoded the token would either be `sntrys_{encoded_bisquit}` or +`sntrys_{secret_key}.{base64_encoded_facts}` depending on the format chosen. Alternatively +that JSON stucture could be encoded into a JWT token with sufficient restrictions. + +## Transmitting Tokens + +Tokens are sent to the target sentry as `Bearer` token like normal. The server uses the +`sntrys_` prefix to automatically detect a structural token. + +## Parsing Tokens + +Clients are strongly encouraged to parse out the containing structure of the token and +to use this information to route requests. The most important keys in the structure +are: + +* `site`: references the target API URL that should be used. A token will always have a + site in it and clients are not supposed to provide a fallback. +* `org`: a token is uniquely bound to an org, so the slug of that org is also always + contained. Note that the slug is used rather than an org ID as the clients typically + need these slugs to create API requests. +* `projects`: a token can be valid for more than one project. For operations such as + source map uploads it's benefitial to issue tokens bound to a single project in which + case the upload experience does not require providing the project slugs. + +## Token Issuance + +The purpose of this change is to allow any organization member to issue tokens with little +overhead. As users can already issue tokens which shocking levels of access to any of the +orgs they are a member of there is a lot of room for improvement. + +The proposed initial step is to only permit token issuance to support uploads and to permit +all users in the org to issue such tokens. The tokens can be shown in the org's +"Developer Settings" page under a new tab called "Tokens". + +Such simple token issuance can then also take place in wizards and documentation pages +This for instance would change this complex webpack config from the docs which requires +matching `org`, `project` and manually creating a sentry token: + +```javascript +const SentryWebpackPlugin = require("@sentry/webpack-plugin"); + +module.exports = { + devtool: "source-map", + plugins: [ + new SentryWebpackPlugin({ + org: "demo-org", + project: "demo-project", + include: "./dist", + // Auth tokens can be obtained from https://sentry.io/settings/account/api/auth-tokens/ + // and needs the `project:releases` and `org:read` scopes + authToken: process.env.SENTRY_AUTH_TOKEN, + }), + ], +}; +``` + +To a much more simplified version: + +```javascript +const SentryWebpackPlugin = require("@sentry/webpack-plugin"); + +module.exports = { + devtool: "source-map", + plugins: [ + new SentryWebpackPlugin({ + authToken: "AUTO GENERATED TOKEN HERE", + include: "./dist", + }), + ], +}; +``` + +# Unresolved questions + +- Is Biscuit a reasonable standard? Do we want to give it a try? + - Supporting Biscuit makes revocations more complex as tokens are somewhat malleable. + - A benefit of Biscuits would be that they can be trivially temporarily restricted upon + use which limits the dangers of some forms of token loss (eg: leak out in logs). + From 2f9c89f851fce7100e12d717ab8629c575f23ed4 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 15 May 2023 13:44:41 +0200 Subject: [PATCH 02/11] Added Hybrid Cloud / Single Tenant note --- text/0091-ci-upload-tokens.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/text/0091-ci-upload-tokens.md b/text/0091-ci-upload-tokens.md index 5f8696aa..34699bc4 100644 --- a/text/0091-ci-upload-tokens.md +++ b/text/0091-ci-upload-tokens.md @@ -34,6 +34,11 @@ still too high for many customers to make the necessary investment into getting uploaded. From a documentation writing and onboarding experience, it's also not clear with the current system how the experience can be improved. +Additionally both Hybrid Cloud and Single Tenant would greatly benefit from automatically +routing to the right URLs. Today the documentation is very quiet about how to get this +system to work on a single tenant installation and customers are often required to work +with CS to get source maps working. + # Technical Implementation The motivation is to add a new kind of token to Sentry which are fundamentally per-organization From 1008238aeef250faf6f99ff0ff4df0e6eff43cd0 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 15 May 2023 15:03:55 +0200 Subject: [PATCH 03/11] Updated spec to mention JWT --- text/0091-ci-upload-tokens.md | 110 ++++++++++++++++++++++++++-------- 1 file changed, 86 insertions(+), 24 deletions(-) diff --git a/text/0091-ci-upload-tokens.md b/text/0091-ci-upload-tokens.md index 34699bc4..55f0ffd2 100644 --- a/text/0091-ci-upload-tokens.md +++ b/text/0091-ci-upload-tokens.md @@ -61,22 +61,34 @@ _policies_. One of the benefits of having the tokens carry this data is that the token alone has enough information available to route to a Sentry installation. This means that `sentry-cli` or any other tool _just_ needs the token to even determine the host that the token should be -sent against. +sent against. This benefit also applies to JWT or PASETO tokens which can be considered +for this as well. The RFC here thus proposes two potential options: A **Biscuit** token +format and a **JWT** token format. -The token then gets a prefix `sntrys_` (sentry structure) to make it possible to -detect it by security scrapers. Anyone handling such a token is required to check -for the `sntrys_` prefix and disregard it before parsing it. - -It's unclear at the moment if Biscuit is a good choice. There is an alternative where -instead of expressing everything in Biscuit tokens, the token gains a base64 encoded -JSON payload that the client can parse containing just the facts. +A serialized token is added a custom prefix `sntrys_` (sentry structure) to make +it possible to detect it by security scrapers. Anyone handling such a token is +required to check for the `sntrys_` prefix and disregard it before parsing it. This +can also be used by the client side to detect a structural token if the client is +interested in extracting data from the token. ## Token Facts -Tokens in Biscuit contain facts. Each fact can later be referenced by the policy and -further restrictions can be placed on the token. For instance this is a basic set of -token facts for a token generated in the UI that has permissions to releases and -org read. +We want to encode certain information into the tokens. In Biscuit terms these are called +_facts_. The following facts exist: + +* `site`: references the target API URL that should be used. A token will always have a + site in it and clients are not supposed to provide a fallback. For instance this + would be `https://myorg.sentry.io/`. +* `org`: a token is uniquely bound to an org, so the slug of that org is also always + contained. Note that the slug is used rather than an org ID as the clients typically + need these slugs to create API requests. +* `projects`: a token can be valid for more than one project. For operations such as + source map uploads it's benefitial to issue tokens bound to a single project in which + case the upload experience does not require providing the project slugs. + +### Biscuit Token Encoding + +For biscuit tokens the following format could be used: ```javascript site("https://myorg.sentry.io"); @@ -86,25 +98,29 @@ scope("project:releases"); scope("org:read"); ``` -Alternatively we we decide not to consider Biscuit the facts can be encoded into -a JSON structure. Note that in this case we would not transmit the scopes. +Signed and encoded a biscuit token looks like `sntrys_{encoded_biscuit}`. + +### JWT Token Encoding + +For JWT the facts could be encoded as custom claims: ```json { - "site": "https://myorg.sentry.io", - "org": "myorg", - "projects": ["myproject"] + "iss": "sentry.io", + "iat": 1684154626, + "sentry_site": "https://myorg.sentry.io/", + "sentry_org": "myorg", + "sentry_projects": ["myproject"] } ``` -Encoded the token would either be `sntrys_{encoded_bisquit}` or -`sntrys_{secret_key}.{base64_encoded_facts}` depending on the format chosen. Alternatively -that JSON stucture could be encoded into a JWT token with sufficient restrictions. +Encoded the token would either be `sntrys_{encoded_jwt}`. ## Transmitting Tokens Tokens are sent to the target sentry as `Bearer` token like normal. The server uses the -`sntrys_` prefix to automatically detect a structural token. +`sntrys_` prefix to automatically detect a structural token. For existing tools that are +unaware of the structure behind structural tokens nothing changes. ## Parsing Tokens @@ -131,9 +147,14 @@ The proposed initial step is to only permit token issuance to support uploads an all users in the org to issue such tokens. The tokens can be shown in the org's "Developer Settings" page under a new tab called "Tokens". -Such simple token issuance can then also take place in wizards and documentation pages +Such simple token issuance can then also take place in wizards and documentation pages. + +# How To Teach + +Structural tokens change what needs to be communicated to users quite a bit. In particular +less information is necessary for tools that are compatible with structural tokens. This for instance would change this complex webpack config from the docs which requires -matching `org`, `project` and manually creating a sentry token: +matching `org`, `project` and manually creating a sentry token today: ```javascript const SentryWebpackPlugin = require("@sentry/webpack-plugin"); @@ -153,7 +174,8 @@ module.exports = { }; ``` -To a much more simplified version: +With structural tokens this can be changed to a much more simplified version which also +correctly handles Single Tenant: ```javascript const SentryWebpackPlugin = require("@sentry/webpack-plugin"); @@ -169,6 +191,46 @@ module.exports = { }; ``` +There are however some cases where manual configuration would still be necessary: + +* **Multi project tokens:** if a token contains more than one project, it's unclear if + tools can handle this transparently. In that case sentry-cli in particular is encouraged + to fail with an error and ask the user to explicitly configure the slug of the project + to use. +* **Legacy tools:** for tools not using sentry-cli but using the API directly, there might + be a transitionary phase until the tool supports structural tokens. In that case the + documentation would need to point out the correct way to configure this. The same applies + to old installations of sentry-cli. + +# Order of Execution + +1. The most important piece is the new token. As it behaves like any other token there is no + immediate necessity for a tool to add support for structural tokens. +2. Add a user interface to issue these new tokens on an org level. +3. Add a user interface to issue these new tokens right from the documentation. +4. Add support for structural tokens to sentry-cli to allow `org` and `project` to be made optional. +5. Change documentation to no longer show `org` and `project` for tool config. + +# Why not DSNs? + +Originally the idea came up to directly use DSNs for uploads. With debug IDs there is some +potential to enable this as most of the system is write once and most indexing is now based on +globally unique IDs. However this today does not work for a handful of reasons: + +1. Overwrites: DSNs are public and so someone who wants to disrupt a customer would be able to + disrupt their processing by uploading invalid source maps or other broken files to a customer. +2. DNSs do not have enough routing information: while a DSN encodes some information, it's only + possible to go from a DSN to the ingestion system but not the API layer. A system could be + added to relay to resolve the slugs and API URLs underpinning a DSN, but would reveal + previously private information (the slugs) and requires a pre-flight to relay before making + a request. +3. DSN auth would really only work for source map uploads and debug file uploads, it could not be + extended to other CI actions such as codecov report uploads or release creation due to the + abuse potential caused by public DSNs. +4. DSNs are limited to a single project and in some cases that might not be ideal. In particular + for frontend + backend deployment scenarios being able to use one token to manage releases + across projects might be desirable. + # Unresolved questions - Is Biscuit a reasonable standard? Do we want to give it a try? From f55b237e4313ba85da1614748722eac7703dfa50 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 15 May 2023 15:08:26 +0200 Subject: [PATCH 04/11] Added discussion points --- text/0091-ci-upload-tokens.md | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/text/0091-ci-upload-tokens.md b/text/0091-ci-upload-tokens.md index 55f0ffd2..4d8c91d7 100644 --- a/text/0091-ci-upload-tokens.md +++ b/text/0091-ci-upload-tokens.md @@ -211,7 +211,11 @@ There are however some cases where manual configuration would still be necessary 4. Add support for structural tokens to sentry-cli to allow `org` and `project` to be made optional. 5. Change documentation to no longer show `org` and `project` for tool config. -# Why not DSNs? +# Discussion + +Addressing some questions that came up: + +## Why not DSNs? Originally the idea came up to directly use DSNs for uploads. With debug IDs there is some potential to enable this as most of the system is write once and most indexing is now based on @@ -231,6 +235,20 @@ globally unique IDs. However this today does not work for a handful of reasons: for frontend + backend deployment scenarios being able to use one token to manage releases across projects might be desirable. +## Why not PASETO? + +PASETO as an alternative to JWT can be an option. This should probably be decided based on what +has most support. This proposal really only uses JWT for serialization of meta information, the +actual validation of the JWT tokens only ever happens on the server side in which case the system +can fully fall back to validating them based on what's stored in the database. + +## Why Biscuit? + +It's unclear if Biscuit is a great solution. There is a lot of complexity in it and tooling support +is not great. However Biscuit is a potentially quite exiting idea because it would permit tools +like sentry-cli to work with temporarily restricted tokens which reduces the chance of token leakage. +The complexity of Biscuit however might be so prohibitive that it's not an appealing choice. + # Unresolved questions - Is Biscuit a reasonable standard? Do we want to give it a try? From 6d83429004f7f2216cbca16620a13493722cce68 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 15 May 2023 15:53:32 +0200 Subject: [PATCH 05/11] Added JWT example --- text/0091-ci-upload-tokens.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/text/0091-ci-upload-tokens.md b/text/0091-ci-upload-tokens.md index 4d8c91d7..a79c32c7 100644 --- a/text/0091-ci-upload-tokens.md +++ b/text/0091-ci-upload-tokens.md @@ -137,6 +137,21 @@ are: source map uploads it's benefitial to issue tokens bound to a single project in which case the upload experience does not require providing the project slugs. +An example of this with a JWT token: + +``` +>>> import jwt +>>> tok = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzZW50cnkuaW8iLCJpYXQiOjE2ODQxNTQ2MjYsInNlbnRyeV9zaXRlIjoiaHR0cHM6Ly9teW9yZy5zZW50cnkuaW8vIiwic2VudHJ5X29yZyI6Im15b3JnIiwic2VudHJ5X3Byb2plY3RzIjpbIm15cHJvamVjdCJdfQ.ROnK3f72jGbH2CLkmswMIxXP1qZHDish9lN6kfCR0DU" +>>> jwt.decode(tok, options={"verify_signature": False}) +{ + 'iss': 'sentry.io', + 'iat': 1684154626, + 'sentry_site': 'https://myorg.sentry.io/', + 'sentry_org': 'myorg', + 'sentry_projects': ['myproject'] +} +``` + ## Token Issuance The purpose of this change is to allow any organization member to issue tokens with little From c7a7bcdf74a92fead341f6de7b503d4ecb8ef24d Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 15 May 2023 15:53:51 +0200 Subject: [PATCH 06/11] Syntax highlighting --- text/0091-ci-upload-tokens.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0091-ci-upload-tokens.md b/text/0091-ci-upload-tokens.md index a79c32c7..5a0350f4 100644 --- a/text/0091-ci-upload-tokens.md +++ b/text/0091-ci-upload-tokens.md @@ -139,7 +139,7 @@ are: An example of this with a JWT token: -``` +```python >>> import jwt >>> tok = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzZW50cnkuaW8iLCJpYXQiOjE2ODQxNTQ2MjYsInNlbnRyeV9zaXRlIjoiaHR0cHM6Ly9teW9yZy5zZW50cnkuaW8vIiwic2VudHJ5X29yZyI6Im15b3JnIiwic2VudHJ5X3Byb2plY3RzIjpbIm15cHJvamVjdCJdfQ.ROnK3f72jGbH2CLkmswMIxXP1qZHDish9lN6kfCR0DU" >>> jwt.decode(tok, options={"verify_signature": False}) From 56396998deb2184899a154706b09a3c31b45343e Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 15 May 2023 16:01:37 +0200 Subject: [PATCH 07/11] Chop off prefix --- text/0091-ci-upload-tokens.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/text/0091-ci-upload-tokens.md b/text/0091-ci-upload-tokens.md index 5a0350f4..cdb1510c 100644 --- a/text/0091-ci-upload-tokens.md +++ b/text/0091-ci-upload-tokens.md @@ -141,8 +141,8 @@ An example of this with a JWT token: ```python >>> import jwt ->>> tok = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzZW50cnkuaW8iLCJpYXQiOjE2ODQxNTQ2MjYsInNlbnRyeV9zaXRlIjoiaHR0cHM6Ly9teW9yZy5zZW50cnkuaW8vIiwic2VudHJ5X29yZyI6Im15b3JnIiwic2VudHJ5X3Byb2plY3RzIjpbIm15cHJvamVjdCJdfQ.ROnK3f72jGbH2CLkmswMIxXP1qZHDish9lN6kfCR0DU" ->>> jwt.decode(tok, options={"verify_signature": False}) +>>> tok = "sntrys_eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzZW50cnkuaW8iLCJpYXQiOjE2ODQxNTQ2MjYsInNlbnRyeV9zaXRlIjoiaHR0cHM6Ly9teW9yZy5zZW50cnkuaW8vIiwic2VudHJ5X29yZyI6Im15b3JnIiwic2VudHJ5X3Byb2plY3RzIjpbIm15cHJvamVjdCJdfQ.ROnK3f72jGbH2CLkmswMIxXP1qZHDish9lN6kfCR0DU" +>>> jwt.decode(tok[7:], options={"verify_signature": False}) { 'iss': 'sentry.io', 'iat': 1684154626, From 179195a99d3272caa80aa2e9a09727fc1b02297c Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 15 May 2023 17:29:50 +0200 Subject: [PATCH 08/11] Apply suggestions from code review Co-authored-by: Mark Story --- text/0091-ci-upload-tokens.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/text/0091-ci-upload-tokens.md b/text/0091-ci-upload-tokens.md index cdb1510c..821bcb98 100644 --- a/text/0091-ci-upload-tokens.md +++ b/text/0091-ci-upload-tokens.md @@ -16,7 +16,7 @@ Today there are two ways to get access tokens for use with sentry-cli: 1. per user access tokens 2. internal organization integrations -Either one are not great. The per user token is easy to get access to (which is why +Both are not great. The per user token is easy to get access to (which is why they are preferred in the docs still) but they run into the risk that a user departs an organization and an integration stops working. The organization integration flow is complex and requires elevated privileges. Either of those options have the additional @@ -83,7 +83,7 @@ _facts_. The following facts exist: contained. Note that the slug is used rather than an org ID as the clients typically need these slugs to create API requests. * `projects`: a token can be valid for more than one project. For operations such as - source map uploads it's benefitial to issue tokens bound to a single project in which + source map uploads it's beneficial to issue tokens bound to a single project in which case the upload experience does not require providing the project slugs. ### Biscuit Token Encoding @@ -155,7 +155,7 @@ An example of this with a JWT token: ## Token Issuance The purpose of this change is to allow any organization member to issue tokens with little -overhead. As users can already issue tokens which shocking levels of access to any of the +overhead. As users can already issue tokens with shocking levels of access to any of the orgs they are a member of there is a lot of room for improvement. The proposed initial step is to only permit token issuance to support uploads and to permit @@ -238,7 +238,7 @@ globally unique IDs. However this today does not work for a handful of reasons: 1. Overwrites: DSNs are public and so someone who wants to disrupt a customer would be able to disrupt their processing by uploading invalid source maps or other broken files to a customer. -2. DNSs do not have enough routing information: while a DSN encodes some information, it's only +2. DSNs do not have enough routing information: while a DSN encodes some information, it's only possible to go from a DSN to the ingestion system but not the API layer. A system could be added to relay to resolve the slugs and API URLs underpinning a DSN, but would reveal previously private information (the slugs) and requires a pre-flight to relay before making From 5eff5662f80b0a8a7d09fb1c6751a2a2e6314336 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 22 May 2023 13:26:22 +0200 Subject: [PATCH 09/11] No biscuit --- text/0091-ci-upload-tokens.md | 68 +++++++++++------------------------ 1 file changed, 21 insertions(+), 47 deletions(-) diff --git a/text/0091-ci-upload-tokens.md b/text/0091-ci-upload-tokens.md index 821bcb98..8f3d9167 100644 --- a/text/0091-ci-upload-tokens.md +++ b/text/0091-ci-upload-tokens.md @@ -49,21 +49,19 @@ data. For the purpose of this document they are called **structural tokens**. ## Token Format -The proposed token format is undecided so far. The goals of the token align generally with -both [Macaroons](http://macaroons.io/) and [Biscuit](https://www.biscuitsec.org). Unfortunately -the former standard has never seen much attention, and the latter is pretty new and not -particularly proven. Either system however permits adding additional restrictions to the -token which make them a perfect choice for the use in our pipeline. Biscuits however seem -quite promising. The idea of biscuits is that the token itself holds _facts_ which and -can be further constraints via _checks_ and they are checked against a datalog inspired -_policies_. - -One of the benefits of having the tokens carry this data is that the token alone has enough +The proposed token format is to leverage JWT as serialization format. The goals of the +token align generally with both [Macaroons](http://macaroons.io/) and +[Biscuit](https://www.biscuitsec.org) but unfortunately the former standard has never seen +much attention, and the latter is pretty new, not particularly proven and very complex. +Either system however permits adding additional restrictions to the token which make them +a very interesting choice for the use in our pipeline. + +One of the benefits of having the tokens carry additional data is that the token alone has enough information available to route to a Sentry installation. This means that `sentry-cli` or any other tool _just_ needs the token to even determine the host that the token should be sent against. This benefit also applies to JWT or PASETO tokens which can be considered -for this as well. The RFC here thus proposes two potential options: A **Biscuit** token -format and a **JWT** token format. +for this as well. The RFC here thus proposes to encode this data into a regular **JWT** +token. A serialized token is added a custom prefix `sntrys_` (sentry structure) to make it possible to detect it by security scrapers. Anyone handling such a token is @@ -73,35 +71,18 @@ interested in extracting data from the token. ## Token Facts -We want to encode certain information into the tokens. In Biscuit terms these are called -_facts_. The following facts exist: +We want to encode certain information into the tokens. The following attributes are defined: -* `site`: references the target API URL that should be used. A token will always have a +* `sentry_site`: references the target API URL that should be used. A token will always have a site in it and clients are not supposed to provide a fallback. For instance this would be `https://myorg.sentry.io/`. -* `org`: a token is uniquely bound to an org, so the slug of that org is also always +* `sentry_org`: a token is uniquely bound to an org, so the slug of that org is also always contained. Note that the slug is used rather than an org ID as the clients typically need these slugs to create API requests. -* `projects`: a token can be valid for more than one project. For operations such as +* `sentry_projects`: a token can be valid for more than one project. For operations such as source map uploads it's beneficial to issue tokens bound to a single project in which case the upload experience does not require providing the project slugs. -### Biscuit Token Encoding - -For biscuit tokens the following format could be used: - -```javascript -site("https://myorg.sentry.io"); -org("myorg"); -project("myproject"); -scope("project:releases"); -scope("org:read"); -``` - -Signed and encoded a biscuit token looks like `sntrys_{encoded_biscuit}`. - -### JWT Token Encoding - For JWT the facts could be encoded as custom claims: ```json @@ -114,7 +95,7 @@ For JWT the facts could be encoded as custom claims: } ``` -Encoded the token would either be `sntrys_{encoded_jwt}`. +Encoded the token then is be `sntrys_{encoded_jwt}`. ## Transmitting Tokens @@ -125,11 +106,12 @@ unaware of the structure behind structural tokens nothing changes. ## Parsing Tokens Clients are strongly encouraged to parse out the containing structure of the token and -to use this information to route requests. The most important keys in the structure -are: +to use this information to route requests. For the keys the following rules apply: -* `site`: references the target API URL that should be used. A token will always have a - site in it and clients are not supposed to provide a fallback. +* `sentry_site`: references the target API URL that should be used. A token + will always have a site in it and clients are not supposed to provide an + automatic fallback. If a site is not provided, one from the client config + should be picked up (typically `sentry.io`). * `org`: a token is uniquely bound to an org, so the slug of that org is also always contained. Note that the slug is used rather than an org ID as the clients typically need these slugs to create API requests. @@ -257,17 +239,9 @@ has most support. This proposal really only uses JWT for serialization of meta actual validation of the JWT tokens only ever happens on the server side in which case the system can fully fall back to validating them based on what's stored in the database. -## Why Biscuit? +## Why Not Biscuit? It's unclear if Biscuit is a great solution. There is a lot of complexity in it and tooling support is not great. However Biscuit is a potentially quite exiting idea because it would permit tools like sentry-cli to work with temporarily restricted tokens which reduces the chance of token leakage. The complexity of Biscuit however might be so prohibitive that it's not an appealing choice. - -# Unresolved questions - -- Is Biscuit a reasonable standard? Do we want to give it a try? - - Supporting Biscuit makes revocations more complex as tokens are somewhat malleable. - - A benefit of Biscuits would be that they can be trivially temporarily restricted upon - use which limits the dangers of some forms of token loss (eg: leak out in logs). - From 404b1515b78d2aecb912d103423e7de1945e84ee Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 23 May 2023 17:30:02 +0200 Subject: [PATCH 10/11] Update RFC on recent conversations --- text/0091-ci-upload-tokens.md | 60 ++++++++++++++++++++++++----------- 1 file changed, 42 insertions(+), 18 deletions(-) diff --git a/text/0091-ci-upload-tokens.md b/text/0091-ci-upload-tokens.md index 8f3d9167..8a347876 100644 --- a/text/0091-ci-upload-tokens.md +++ b/text/0091-ci-upload-tokens.md @@ -79,9 +79,13 @@ We want to encode certain information into the tokens. The following attributes * `sentry_org`: a token is uniquely bound to an org, so the slug of that org is also always contained. Note that the slug is used rather than an org ID as the clients typically need these slugs to create API requests. -* `sentry_projects`: a token can be valid for more than one project. For operations such as - source map uploads it's beneficial to issue tokens bound to a single project in which - case the upload experience does not require providing the project slugs. + +Potential fields: + +* `sentry_projects`: normally a token is valid for the entire org, but it could potentially + be restricted. For operations such as source map uploads it might be interesting to issue + tokens bound to a single project in which case the upload experience does not require + providing the project slugs. However we currently do not want to start with this. For JWT the facts could be encoded as custom claims: @@ -90,8 +94,7 @@ For JWT the facts could be encoded as custom claims: "iss": "sentry.io", "iat": 1684154626, "sentry_site": "https://myorg.sentry.io/", - "sentry_org": "myorg", - "sentry_projects": ["myproject"] + "sentry_org": "myorg" } ``` @@ -115,9 +118,6 @@ to use this information to route requests. For the keys the following rules app * `org`: a token is uniquely bound to an org, so the slug of that org is also always contained. Note that the slug is used rather than an org ID as the clients typically need these slugs to create API requests. -* `projects`: a token can be valid for more than one project. For operations such as - source map uploads it's benefitial to issue tokens bound to a single project in which - case the upload experience does not require providing the project slugs. An example of this with a JWT token: @@ -130,7 +130,7 @@ An example of this with a JWT token: 'iat': 1684154626, 'sentry_site': 'https://myorg.sentry.io/', 'sentry_org': 'myorg', - 'sentry_projects': ['myproject'] + 'sentry_projects': ['myproject'] # not used currently } ``` @@ -160,6 +160,7 @@ module.exports = { devtool: "source-map", plugins: [ new SentryWebpackPlugin({ + url: "https://sentry.io/", // defaults to sentry.io org: "demo-org", project: "demo-project", include: "./dist", @@ -182,22 +183,21 @@ module.exports = { plugins: [ new SentryWebpackPlugin({ authToken: "AUTO GENERATED TOKEN HERE", + project: "demo-project", include: "./dist", }), ], }; ``` -There are however some cases where manual configuration would still be necessary: +Some manual configuration remains as we still want ask users to provide the slug +of the project explicitly to allow cross-org token issuance by default. -* **Multi project tokens:** if a token contains more than one project, it's unclear if - tools can handle this transparently. In that case sentry-cli in particular is encouraged - to fail with an error and ask the user to explicitly configure the slug of the project - to use. -* **Legacy tools:** for tools not using sentry-cli but using the API directly, there might - be a transitionary phase until the tool supports structural tokens. In that case the - documentation would need to point out the correct way to configure this. The same applies - to old installations of sentry-cli. +Additionally **legacy tools** will require more configuration. For tools not +using sentry-cli but using the API directly, there might be a transitionary +phase until the tool supports structural tokens. In that case the documentation +would need to point out the correct way to configure this. The same applies to +old installations of sentry-cli. # Order of Execution @@ -212,6 +212,30 @@ There are however some cases where manual configuration would still be necessary Addressing some questions that came up: +## Project Bound Tokens + +It would be possible to restrict tokens to a single project (or some projects). At a later +point this might still be interesting when the tokens become more potent. For now these +tokens can only be used to upload files which means that the damage that one org member +can do against projects they are not a member of are limited. As such we are willing to +accept the risk of issuing tokens across the entire org. + +This also means that tools will still require the project slug to be provided for operations +that are project bound. Today most of these operations are project bound, but we might want +to investigate ways to bring most of these operations to the org level so that over time this +information can be removed. + +For instance for debug files there is no good reason why these files are not uploaded to +org level to begin with. For source maps the situation is a bit more complex due to the +optional nature of debug IDs. However in an increasing number of cases uploads should +again be possible to the org level. + +The benefit of a cross-org token is that this token can then later be used against other +projects in the same pipeline without having to re-issue the token. For instance a CI job +that first only uploads the frontend source maps might later want to do release creation +for the backend as well. Having an overly restricted token would make this a more painful +change. + ## Why not DSNs? Originally the idea came up to directly use DSNs for uploads. With debug IDs there is some From 7b6cfe0dd997f541bc165ea22863b60796be10ec Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Tue, 13 Jun 2023 14:13:30 +0200 Subject: [PATCH 11/11] Adjustments to RFC #91 (#102) Co-authored-by: Mark Story --- text/0091-ci-upload-tokens.md | 63 +++++++++++++++++++++++------------ 1 file changed, 41 insertions(+), 22 deletions(-) diff --git a/text/0091-ci-upload-tokens.md b/text/0091-ci-upload-tokens.md index 8a347876..a7a0fcc9 100644 --- a/text/0091-ci-upload-tokens.md +++ b/text/0091-ci-upload-tokens.md @@ -45,7 +45,7 @@ The motivation is to add a new kind of token to Sentry which are fundamentally p tokens, but with the ability to carry meta information that tools like sentry-cli can use to improve the user experience. These org level tokens can be created by anyone in the org, they can be given additional restrictions, and they can carry meta information such as routing -data. For the purpose of this document they are called **structural tokens**. +data. For the purpose of this document they are called **structural tokens**. ## Token Format @@ -73,33 +73,37 @@ interested in extracting data from the token. We want to encode certain information into the tokens. The following attributes are defined: -* `sentry_site`: references the target API URL that should be used. A token will always have a - site in it and clients are not supposed to provide a fallback. For instance this - would be `https://myorg.sentry.io/`. +* `iss`: The value `sentry.io` indicates that this is a Sentry Org Auth Token. +* `nonce`: A randomly generated UUID to ensure the token content cannot be guessed. +* `sentry_url`: references the root domain to be used. A token will always have a + url in it and clients are not supposed to provide a fallback. This value can be found in `settings.SENTRY_OPTIONS["system.url-prefix"]`. Some APIs are only available on this URL, not on the region URL (see below). e.x. `https://sentry.io/`. +* `sentry_region_url`: The domain that the organization's API endpoints are available on. This value can be found in `organization.links.regionUrl`. e.x. `http://us.sentry.io`. * `sentry_org`: a token is uniquely bound to an org, so the slug of that org is also always - contained. Note that the slug is used rather than an org ID as the clients typically + contained. Note that the slug is used rather than an org ID as the clients typically need these slugs to create API requests. -Potential fields: - -* `sentry_projects`: normally a token is valid for the entire org, but it could potentially - be restricted. For operations such as source map uploads it might be interesting to issue - tokens bound to a single project in which case the upload experience does not require - providing the project slugs. However we currently do not want to start with this. - -For JWT the facts could be encoded as custom claims: +These facts are encoded in the JWT as custom claims: ```json { "iss": "sentry.io", "iat": 1684154626, - "sentry_site": "https://myorg.sentry.io/", + "nonce": "abcd-efgh-ijkl-mnop", + "sentry_region_url": "https://eu.sentry.io/", + "sentry_url": "https://sentry.io/", "sentry_org": "myorg" } ``` Encoded the token then is be `sntrys_{encoded_jwt}`. +## Token Storage + +Tokens are stored in the database in hashed form, not in plain text. +In addition, we store the last 4 characters of the token in plain text in order to help with identification of tokens. +We also allow to define a `name` for a token for easier identification, +however this may often be auto-generated when e.g. creating a token from the docs or other places. + ## Transmitting Tokens Tokens are sent to the target sentry as `Bearer` token like normal. The server uses the @@ -111,10 +115,9 @@ unaware of the structure behind structural tokens nothing changes. Clients are strongly encouraged to parse out the containing structure of the token and to use this information to route requests. For the keys the following rules apply: -* `sentry_site`: references the target API URL that should be used. A token +* `sentry_url` & `sentry_region_url`: references the target API URL that should be used. A token will always have a site in it and clients are not supposed to provide an - automatic fallback. If a site is not provided, one from the client config - should be picked up (typically `sentry.io`). + automatic fallback. * `org`: a token is uniquely bound to an org, so the slug of that org is also always contained. Note that the slug is used rather than an org ID as the clients typically need these slugs to create API requests. @@ -128,9 +131,9 @@ An example of this with a JWT token: { 'iss': 'sentry.io', 'iat': 1684154626, - 'sentry_site': 'https://myorg.sentry.io/', - 'sentry_org': 'myorg', - 'sentry_projects': ['myproject'] # not used currently + 'sentry_url': 'https://sentry.io/', + 'sentry_region_url': 'https://eu.sentry.io/', + 'sentry_org': 'myorg' } ``` @@ -146,6 +149,18 @@ all users in the org to issue such tokens. The tokens can be shown in the org's Such simple token issuance can then also take place in wizards and documentation pages. +The generated token itself is only visible after creation. Users cannot see the token again later. + +## Token Revocation + +Tokens cannot be deleted, but only revoked (=soft deleted). Only managers & owners may revoke tokens. +Users may be able to delete tokens they created regardless of their role. + +## Editing Tokens + +Only the `name` of the token may be updated after it was created. Any user may update any tokens name. +You cannot update the scope(s) of a token after it was issued. + # How To Teach Structural tokens change what needs to be communicated to users quite a bit. In particular @@ -205,8 +220,8 @@ old installations of sentry-cli. immediate necessity for a tool to add support for structural tokens. 2. Add a user interface to issue these new tokens on an org level. 3. Add a user interface to issue these new tokens right from the documentation. -4. Add support for structural tokens to sentry-cli to allow `org` and `project` to be made optional. -5. Change documentation to no longer show `org` and `project` for tool config. +4. Add support for structural tokens to sentry-cli to allow `org` to be made optional. +5. Change documentation to no longer show `org` & `url` for tool config. # Discussion @@ -269,3 +284,7 @@ It's unclear if Biscuit is a great solution. There is a lot of complexity in it is not great. However Biscuit is a potentially quite exiting idea because it would permit tools like sentry-cli to work with temporarily restricted tokens which reduces the chance of token leakage. The complexity of Biscuit however might be so prohibitive that it's not an appealing choice. + +## Why not include the Project? + +We decided to only encode the org-reference into the token, not the project. This allows CI to extend usage to new/other projects without having to issue a new token. In the future, we may allow to also bind tokens to project(s). But for now, all tokens are org-wide. \ No newline at end of file