goauthentik / authentik

The authentication glue you need.
https://goauthentik.io
Other
13.04k stars 868 forks source link

Client Certificate authentication #2859

Open scheibling opened 2 years ago

scheibling commented 2 years ago

I've been looking into client certificate authentication a bit lately on behalf of work, and I was wondering if this is something you've had a look into at any point. From my end, it looks like it shouldn't need any major implementation in authentik but rather just a stage like the webauthn/totp-stages, with some slight modifications to the go proxy.

I think there would be a couple of benefits to this long-term:

The major part of this as it stands would be an implementation into the go proxy, since that's the part currently handling certificate authentication. I don't have a lot of experience with go as of right now, but if this is something that would appeal to a broader audience I'd be happy to look into a couple of PRs on the subject.

I've managed to run some tests by integrating it into gunicorn separately, but since that server doesn't handle the TLS parts of authentik I don't think that's the way forward.

A basic implementation of this would be:

  1. Go proxy asks for client certificate when accessing certain flows (by URI? or does the proxy have any awareness of states in the application?)
  2. The proxy sets a header (e.g. X-Client-Cert-CN, X-Client-Cert-Issuer, X-Client-Cert-CA-Cert) to the authentik flow
  3. Authentik verifies the certificate against a certificate currently in storage
  4. Uses the certificate CN (configurable mapping) to map the authentication to a user, either as single-factor or multi-factor after sign-on with another stage.
BeryJu commented 2 years ago

duplicate of #2294 but yes that is roughly how I'd implement it too, however there are a couple issues with doing it that way:

Now the last point is technically not an issue since the go proxy can already get the certificates via the API, and it does already do that if you configure a web certificate in a tenant. However, certificates for this would (probably) be configured on a stage level, which would make it a bit harder to correlate.

Allthough I guess the proxy could just fetch and check against all CAs configured in the backend, and set the headers if any of them are valid, and then the checking for the correct CA is done in the stage

The other reason I haven't looked into this too much is that this wouldn't work (easily) with reverse proxies, there'd need to be another setting to trust headers from a range of upstream proxies

scheibling commented 2 years ago

Wouldn't a global setting for trusted proxies, like the native one in Django, be a good idea security-wise anyway? It could probably be used for this as well as securing an instance against certain kinds of attacks where authentik isn't specifically configured to only accept certain hosts, which might be the case with certain configurations.

Although a potential issue with the global/per-host certificate requests is that the user would then either be queried for the certificate at first page load, or on every page load, with pin entry and all depending on the security software involved. I can imagine some scenarios in which the former could be problematic, and the latter isn't really an option.

I'm gonna have a look over how some other applications implement it and see if there are any other alternative strategies.

Edit: A "simpler" (more basic, not easier) implementation would probably be a stage for authentication via HTTP headers, that way a user could inject the headers via their own reverse proxy and handle the authentication themselves. This would still require a trusted proxy functionality though.

e.g. something like this for HAProxy

listen frontend-name
    bind *:443 transparent ssl crt server-cert.pem ca-file cert-ca.crt verify optional crl-file cert.crl
    ...
    acl restricted_path path_beg,url_dec -m beg -i /api/v3/flows/executor/http-header-auth

    http-request set-header X-SSL-Issuer           %{+Q}[ssl_c_i_dn] if restricted_path and { ssl_c_verify } 
    http-request set-header X-SSL-Client-NotBefore %{+Q}[ssl_c_notbefore] if  restricted_path and { ssl_c_verify }
    http-request set-header X-SSL-Client-NotAfter  %{+Q}[ssl_c_notafter] if  restricted_path and { ssl_c_verify }

    # Adding this if the request should be denied entirely if no client certificate is present
    # http-request deny if restricted_path !{ ssl_c_used 1 } || restricted_path !{ ssl_c_verify 0 }
cwilson613 commented 10 months ago

Would love to see this added. I have been enjoying exploring Authentik, and I think its structure and layout are much more intuitive than traditional IDP/Auth/SSO solutions.

Unfortunately, I need the ability to issue end-user x.509 certificates and use PKI for direct authentication, and currently to do that I’d have to set up some wonky stage to verify the cert as a second factor and use the CN to set the username.

I have been dragging my feet before standing up KeyCloak for this purpose, because I’d rather just keep using Authentik. Can’t blame anybody for not spending a lot of time and effort on very low-volume requests

ne0YT commented 10 months ago

is there any solution to have a client logged in foreve rcurrently? (client is on Wndows Server 2022 and uses chrome browser). sadly configuring a session timeout in authentik to 9999 days doesn't work.

OGKevin commented 9 months ago

Is my understanding correct that the header suggestion that @scheibling made is currently not possible? I’ve configured my proxy to indeed send the email, and fingerprint of the cert to Authenik via headers. But im failing to figure out how to configure Authentik to read these headers, find and set the pending user, and execute a login.

It seems that only the build in identification stage can be used? Setting pending user in an expression policy for example does not work 🤔

allebone commented 8 months ago

Lots of users needing Smartcard/PIV/PKI Auth, which works in KC. I don’t have any skills in development, but I’ve implemented it a ton of times. If anyone goes through with it, don’t forget:

  1. you need a way to trust third party CAs by uploading root/intermediate certs.
  2. you have to support CRL OR OSCP. OCSP in keycloak is not ideal, right now. If the root OCSP is slow or times out, it fails the Auth flow. It would be way cool for someone to figure out how to do OCSP caching. Google was working on it, but nothing I’ve seen in a project yet. CRL is big enterprises are huge, btw.
  3. the certs dont all have the same attributes. So you’ll need to fish certain fields out of SAN or CN, but it’s not always 100% certain which in each org.
wjbrf commented 7 months ago

Has there been much activity on this? I've been looking at this but PKI authentication is pretty much a requirement, especially compared to Keycloak that does have a PKI authentication option.

I feel like all the issues mentioned here are non-issues in my environment, some of them not even central to what authentication service needs to do itself to be fair.

My environment has all the ca's available for validation. In fact we primarily use PKI authentication.

OSCP, I'm not sure about but there is a central API used for certificate status checks.

Another party does the issuing certificates, the authentication server only need to validate so then we can issue a bearer token for our services.

lordwelch commented 3 months ago

You can actually implement this with the login stage and a an expression policy and nginx. It is expected that you have secured the path from nginx to authentik and that all access to authentik goes through nginx. One way to achieve this with docker is by only publishing the authentik port on localhost -p 127.0.0.1:9443:9443

  1. Configure nginx
    http {
    ...
    server {
    ...
    ssl_verify_client optional;
    ssl_verify_depth 5;
    ssl_client_certificate /etc/nginx/client_cas.pem;
    # these aren't required but they are recommended
    ssl_ocsp          on;
    resolver          1.1.1.1;
    ...
    }
    }
  2. Add an new policy x509 Certificate Authentication.
    
    import unicodedata
    import urllib

from authentik.core.models import User from cryptography import x509

OID_UPN = x509.ObjectIdentifier("1.3.6.1.4.1.311.20.2.3")

def remove_control_characters(s): return "".join(ch for ch in s if unicodedata.category(ch)[0] != "C")

def identify_user(dn: str, upn: str, email: str) -> User | None: if upn and (user := ak_user_by(attributescertupn=upn)): context["auth_method"] = "x509" context["auth_method_args"] = dict(dn=dn, upn=upn, email=email) return user if email and (user := ak_user_by(email=email)): context["auth_method"] = "x509" context["auth_method_args"] = dict(dn=dn, upn=upn, email=email) return user if dn and (user := ak_user_by(attributescertdn=dn)): context["auth_method"] = "x509" context["auth_method_args"] = dict(dn=dn, upn=upn, email=email) return user return None

def x509_cert() -> bool: if "X509_CERT" not in request.http_request.headers or not request.http_request.headers["X509_CERT"]:

ak_create_event("No Certificate Found", client=request.http_request.headers['X-Forwarded-For'])

    return False
encoded_certs = request.http_request.headers["X509_CERT"]
try:
    cert = x509.load_pem_x509_certificate(urllib.parse.unquote(encoded_certs).encode("ascii"))
except ValueError:
    ak_create_event("Failed to parse certificates", certs=encoded_certs)
    return False

info = {}
info["issuer"] = cert.issuer
info["sn"] = str(cert.serial_number)
info["subject"] = cert.subject.rfc4514_string()
info["exts"] = []
# for ext in cert.extensions:
#     info['exts'].append(ext)
sub_alt_name: x509.SubjectAlternativeName = cert.extensions.get_extension_for_oid(x509.OID_SUBJECT_ALTERNATIVE_NAME).value
info["rfc822name"] = sub_alt_name.get_values_for_type(x509.RFC822Name)
if info["rfc822name"]:
    info["rfc822name"] = remove_control_characters(info["rfc822name"][0].strip())
else:
    del info["rfc822name"]

# the upn is what microsoft ad uses to match accounts for smartcard login
info["upn"] = [x for x in sub_alt_name.get_values_for_type(x509.OtherName) if x.type_id == OID_UPN]
if info["upn"]:
    info["upn"] = remove_control_characters(info["upn"][0].value.decode("utf-8", errors="ignore").strip())
else:
    del info["upn"]
context["info"] = info

ak_create_event("CERT DETECTED", **info)

if not (
    user := identify_user(dn=info.get("subject", ""), upn=info.get("upn", ""), email=info.get("rfc822name", ""))
):
    return False

plan = request.context.get("flow_plan")

plan.context["pending_user"] = user
plan.markers = plan.markers[:1]
plan.bindings = plan.bindings[:1]

return True

return x509_cert()

3. Add a login stage at the beginning of the default authentication flow.
  Ensure that `Evaluate when stage is run` is checked on the stage binding
4. Bind the `x509 Certificate Authentication` policy.
  Ensure that `Failure Result` is set to `Pass` in the policy binding.

This will match on email or an a user with a matching attribute on their account like this:
```yaml
email: user@domain.com
cert:
  upn: user@domain
  dn: CN=user, OU=domain
drduker commented 4 weeks ago

lordwelch , you wouldn't happen to have an example of this would you @lordwelch ? Like a simple docker compose or local kind cluster example?

scheibling commented 4 weeks ago

Correct me if I'm wrong, but this example would require a certificate to connect to authentik and not just for specifically a cert auth step?

Or do you have the cert auth setup on a separate domain in the example above @lordwelch ?

drduker commented 3 weeks ago

@lordwelch - the solution above would be awesome, if i could get it working! lol

BeryJu commented 3 weeks ago

what @lordwelch posted is pretty close to what the final support for this would look like. Most likely would be a stage that can pre-fills the user identifier in the flow plan and as such could skip the identification stage. Some of the things that are still missing/need to be figured out for a full feature support: