forked from TrueCloudLab/certificates
Add github response to frequenty asked questions doc
- security risks of exposing the OAuth Client Secret in the output of `step ca provisioner list`
This commit is contained in:
parent
6736ddee69
commit
b5699892ad
1 changed files with 149 additions and 0 deletions
|
@ -104,6 +104,155 @@ designs, to help with this. If you integrate with our other tools its easy to
|
|||
start with a manual identity proofing mechanism and move to a more sophisticated
|
||||
automated method as your system grows.
|
||||
|
||||
## What are the security risks of exposing the OAuth Client Secret in the output of `step ca provisioner list`?
|
||||
|
||||
It would be nice if we could have the CA operate as an OAuth confidential
|
||||
client, keeping the client secret private and redirecting back to the CA
|
||||
instead of to loopback. But, to be clear, this is not an abuse of the OAuth
|
||||
spec. The way this was implemented in step, as an OAuth native application
|
||||
using a public client, is standard, was intentional, (mostly) conforms to best
|
||||
current practices, and the flow we're using is widely used in practice. A
|
||||
confidential client is (strictly?) more secure. But a public client that
|
||||
redirects to loopback isn’t a significant security risk under a normal threat
|
||||
model.
|
||||
|
||||
### The current flow
|
||||
The advantage of the current flow is that it’s more general purpose. For
|
||||
example, `step oauth` works without any additional infrastructure. An issued
|
||||
access token can be used from the command line, and OIDC identity tokens can be
|
||||
safely used to authenticate to remote services (including remote services that
|
||||
don’t speak OAuth OIDC, or don’t even speak HTTP, but can validate a
|
||||
JWT). `step-ca` is one example of a remote service that can authenticate
|
||||
step users via OIDC identity token. You can also use `step crypto jwt verify` to
|
||||
authenticate using OIDC at the command line.
|
||||
|
||||
The particular details of the OAuth flow we selected has pros & cons, as does
|
||||
any flow. The relevant security risks are:
|
||||
|
||||
1. Since the OAuth access token isn’t issued directly to a remote server (e.g.,
|
||||
`step-ca`), remote servers can’t safely use the issued access tokens
|
||||
without significant care. If they did, an attacker might be able to maliciously
|
||||
trick the remote server into using an access token that was issued to a
|
||||
different client.
|
||||
|
||||
2. The redirect back from the OAuth authorization server to the
|
||||
client can be intercepted by another process running on the local machine. This
|
||||
isn’t really necessary though, because...
|
||||
|
||||
3. The `client_secret` is public, so anyone can initiate (and complete) an OAuth
|
||||
flow using our client (but it will always redirect back to 127.0.0.1).
|
||||
|
||||
The first threat is moot since we don't actually use the access token for
|
||||
anything when we're connecting to `step-ca`. Unfortunately there's no way to not
|
||||
get an access token. So we just ignore it.
|
||||
|
||||
Note that it *is* safe to use the access token from the command line to access
|
||||
resources at a remote API. For example, it’s safe to user `step oauth` to obtain
|
||||
an OAuth access token from Google and use it to access Google’s APIs in a bash
|
||||
script.
|
||||
|
||||
More generally, access tokens are for accessing resources (authorization) and
|
||||
are not useful for authenticating a user since they're not audience-addressed.
|
||||
If you and I both have a Google OAuth client, I could get Alice to OAuth into
|
||||
my app and use the issued access token to masquerade as Alice to you. But OIDC
|
||||
identity tokens are audience-addressed. An identity token is a JWT with the
|
||||
`client_id` baked in as the `aud` (audience) parameter. As long as clients check
|
||||
this parameter (which `step-ca` does) they're not susceptible to this attack. In
|
||||
fact, OIDC identity tokens were designed and developed precisely to solve this
|
||||
problem.
|
||||
|
||||
So it's completely safe for one entity to obtain an *identity token* from an IdP
|
||||
on behalf of a user and use it to authenticate to another entity (like `step`
|
||||
does). That's exactly the use case OIDC was designed to support.
|
||||
|
||||
The second and third threats are related. They involve a malicious attempt to
|
||||
initiate an OAuth OIDC flow using our client credentials. There's a lot of
|
||||
analysis we could do here comparing this situation to a non-native (e.g., *web*)
|
||||
client and to other flows (e.g., the *implicit flow*, which also makes the
|
||||
client secret public). Skipping that detail, we know two things for sure:
|
||||
|
||||
1. OAuth flows generally require user consent to complete (e.g., a user has to
|
||||
"approve" an application's authentication / authorization request)
|
||||
|
||||
2. An OAuth flow initiated using our client will always redirect back to 127.0.0.1
|
||||
|
||||
So a malicious attacker trying to obtain an *identity token* needs two things:
|
||||
|
||||
1. They need to get user consent to complete an OAuth flow
|
||||
2. They need to have local access to the user's machine
|
||||
|
||||
This is already a pretty high bar. It’s worth noting, however, that the first
|
||||
part is *much* easier if the user is already logged in and the identity provider
|
||||
is configured to not require consent (i.e., the OAuth flow is automatically
|
||||
completed without the user having to click any buttons). Okta seems to
|
||||
do this for some applications by default.
|
||||
|
||||
It's also worth noting that a process with local access could probably obtain
|
||||
an access/identity token for a *confidential client* without knowing the client
|
||||
secret. That's the main reason I don't think the flow we're using has a
|
||||
meaningful security impact under most threat models. The biggest difference is
|
||||
that attacking a confidential client would probably require privileged (root)
|
||||
access, whereas our flow could be attacked by an unprivileged process. But
|
||||
the fruit of our OAuth flow — the SSH certificate — is also available for
|
||||
use by an unprivileged process running locally via the `ssh-agent`. So the
|
||||
only thing possibly gained is the ability to exfiltrate.
|
||||
|
||||
### Stuff we should consider doing
|
||||
There are at least three OAuth features that are relevant to this discussion.
|
||||
Two have already been mentioned:
|
||||
|
||||
1. OAuth *public clients* for *native applications* can be (er, are *supposed*
|
||||
to be) created without a client secret
|
||||
|
||||
2. Proof Key for Code Exchange (PKCE) helps ensure that the process requesting the access token / identity token is the same process that initiated the flow
|
||||
|
||||
The first feature, clients without secrets, is mostly cosmetic. There's no real
|
||||
difference between a public secret and no secret, except that it's confusing to
|
||||
have something called a "secret" that's not actually secret. (Caveat: IdPs that
|
||||
support "native applications" without secrets typically enforce other rules for
|
||||
these clients — they often require PKCE and might not issue a renew token, for
|
||||
example. But these features can often be turned on/of for other client types,
|
||||
too.)
|
||||
|
||||
The reason we don't assume a *public client* without a secret is that,
|
||||
unfortunately, not all IdPs support them. Significantly, Google does not. In
|
||||
fact, gcloud (Google Cloud's CLI tool) uses OAuth OIDC and uses the exact same
|
||||
technique we're using. If you look at the source you'll find their
|
||||
"NOTSOSECRET" All of that said, we should support "native clients" without
|
||||
secrets at some point.
|
||||
|
||||
We should also implement Proof Key for Code Exchange (PKCE). This has been on
|
||||
our backlog for a while, and it's actually really simple and useful. It's
|
||||
definitely low-hanging fruit. Before initiating the OAuth flow your client
|
||||
generates a random number. It hashes that number and passes the hash to the IdP
|
||||
as part of the authorization request (the URL that users are sent to for
|
||||
login). After authenticating and consenting, when the user is
|
||||
redirected back to the client, the client makes a request to the IdP to get an
|
||||
access token & identity token. In *that* request the client must include the
|
||||
*unhashed* random number. The IdP re-hashes it and compares it to the value it
|
||||
received in the authorization request. If they match, the IdP can be certain
|
||||
that the entity making the access token request is the same entity that
|
||||
initiated the flow. In other words, the request has not been intercepted by
|
||||
some malicious intermediary.
|
||||
|
||||
The last hardening mechanism to be aware of are the `acr` and `amr` parameters.
|
||||
Basically, when the OAuth flow is initiated the client can request that the IdP
|
||||
require consent, do 2FA, and a bunch of other stuff. The issued identity token
|
||||
includes parameters to indicate that these processes did, indeed, occur.
|
||||
Leveraging this mechanism one could configure `step-ca` to check these parameters
|
||||
and be sure that users have consented and undergone a 2FA presence check (e.g.,
|
||||
tapped a security token). Unfortunately, like a bunch of other optional
|
||||
OAuth features, many IdPs (*cough* Google *cough*) don't support this stuff.
|
||||
|
||||
### Summary
|
||||
|
||||
Implementing PKCE should be our highest priority item. Support for "native"
|
||||
clients without secrets would also be nice. Forcing 2FA & consent via `acr` & `amr`
|
||||
is also a good idea. Support for non-native clients that redirect back to the
|
||||
CA, and where the secret is *actually* secret, would also be nice. But it's a
|
||||
bigger architectural change and the security implications aren't actually that
|
||||
severe.
|
||||
|
||||
## I already have PKI in place. Can I use this with my own root certificate?
|
||||
|
||||
Yes. There's a easy way, and a longer but more secure way to do this.
|
||||
|
|
Loading…
Reference in a new issue