## 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
That said, CAs are usually pretty locked down and it's bad practice to move the private key around. So I'm gonna assume that's not an option and give you the more complex instructions to do this "the right way", by generating a CSR for `step-ca`, getting it signed by your existing root, and configuring `step-ca` to use it.
When you run `step ca init` we create a couple artifacts under `~/.step/`. The important ones for us are:
-`~/.step/certs/root_ca.crt` is your root CA certificate
-`~/.step/secrets/root_ca_key` is your root CA signing key
-`~/.step/certs/intermediate_ca.crt` is your intermediate CA cert
-`~/.step/secrets/intermediate_ca_key` is the intermediate signing key used by `step-ca`
The easiest thing to do is to run `step ca init` to get this scaffolding configuration in place, then remove/replace these artifacts with new ones that are tied to your existing root CA.
First, `step-ca` does not actually need the root CA signing key. So you can simply remove that file:
```bash
rm ~/.step/secrets/root_ca_key
```
Next, replace `step-ca`'s root CA cert with your existing root certificate:
Now you need to generate a new signing key and intermediate certificate, signed by your existing root CA. To do that we can use the `step certificate create` subcommand to generate a certificate signing request (CSR) that we'll have your existing root CA sign, producing an intermediate certificate.
To generate those artifacts run:
```bash
step certificate create "Intermediate CA Name" intermediate.csr intermediate_ca_key --csr
```
Next, you'll need to transfer the CSR file (`intermediate.csr`) to your existing root CA and get it signed.
Now you need to get the CSR executed by your existing root CA.