Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Client Certificate ABAC #8012

Open
boris-senapt opened this issue Nov 16, 2023 · 2 comments
Open

Client Certificate ABAC #8012

boris-senapt opened this issue Nov 16, 2023 · 2 comments
Labels
4.x Version 4.x enhancement New feature or request P2 security

Comments

@boris-senapt
Copy link

We can configure Helidon 4 to require client certificate authentication with the following

server:
  port: 8443
  host: 0.0.0.0
  tls:
    client-auth: REQUIRED
    endpoint-identification-algorithm: NONE # This is a bit counter-intuitive
    private-key:
      keystore:
        ...
    trust:
      keystore:
        ...
        trust-store: true

Ignoring the need to set endpoint-identification-algorithm: NONE which is not very intuitive, this works well.

However, coming from a Servlet background I would then expect to be able to do the following

  • register Certificate DN to Role mappings
  • use those Roles in the ABAC authoriser

There is no way to achieve this with Helidon.

Environment Details

  • 4.0.0
  • Helidon SE
  • JDK version: Zulu21.30+15-CA (build 21.0.1+12-LTS)
  • OS: MacOS 14.0

Problem Description

I would like to be able to implement security authorisations using ABAC against clients authenticated using client certificate authentication.

Given Helidon has ABAC the Certificate properties could be exposed to ABDC for dynamic checking against attributes of the Certificate.

In order to get this working in my test lab I have had to do the following

  1. Register a io.helidon.webserver.http.Filter to add the certificate to the request
    @Override
    public void filter(
            final FilterChain chain,
            final RoutingRequest req,
            final RoutingResponse res) {
        final var context = req.context();
        context.get(SecurityContext.class).ifPresent(sec -> {
            final var envBuilder = sec.env().derive();
            req.remotePeer().tlsCertificates().ifPresent(certs -> envBuilder.addAttribute(X509_CERTIFICATE, certs));
            sec.env(envBuilder);
        });
        chain.proceed();
    }

This would seem to be a core feature missing from io.helidon.webserver.security.SecurityContextFilter

  1. Implement an io.helidon.security.spi.AuthenticationProvider

This takes the code from io.helidon.security.providers.httpauth.HttpBasicAuthProvider to configure SecureUserStore instances but then looks up the user by the certificate DN

    @Override
    public AuthenticationResponse authenticate(final ProviderRequest providerRequest) {
        final var foundUser = X509SecurtyFilter.certificates(providerRequest.env())
                .stream()
                .flatMap(Arrays::stream)
                .flatMap(cert -> findUser(cert).stream().map(user -> new SimpleImmutableEntry<>(user, cert)))
                .findFirst();

        return foundUser.map(e -> {
            if (subjectType == SubjectType.USER) {
                return AuthenticationResponse.success(buildSubject(e.getKey(), e.getValue()));
            }
            return AuthenticationResponse.successService(buildSubject(e.getKey(), e.getValue()));
        }).orElseGet(this::invalidUser);
    }

    private Optional<SecureUserStore.User> findUser(final Certificate certificate) {
        if(!(certificate instanceof X509Certificate)) {
            return Optional.empty();
        }
        final var dn = ((X509Certificate) certificate).getSubjectX500Principal();
        return userStores.stream()
                .flatMap(userStore -> userStore.user(dn.getName()).stream())
                .findFirst();
    }

    private Subject buildSubject(final SecureUserStore.User user, final Certificate certificate) {
        Subject.Builder builder = Subject.builder()
                .principal(Principal.builder()
                        .name(user.login())
                        .build())
                .addPublicCredential(
                        X509Credentials.class,
                        new X509Credentials(user.login(), certificate));
        user.roles()
                .forEach(role -> builder.addGrant(Role.create(role)));
        return builder.build();
    }

    private static final class X509Credentials {
        private final String username;
        private final Certificate certificate;

        private X509Credentials(String username, Certificate certificate) {
            this.username = username;
            this.certificate = certificate;
        }
    }
@m0mus m0mus added security enhancement New feature or request 4.x Version 4.x P2 labels Nov 16, 2023
@tomas-langer
Copy link
Member

Hello
the endpoint-identification-algorithm: NONE should not be required at all if you have correctly defined certificates. It is what is called "endpoint verification" in other places, and it is related to the common name of the server certificate - if that is set correctly to the host name used, you do not need to specify this option.

Regarding the feature request, we will investigate it. Right now you can only use the common name from certificate to assert user identity through our header based security provider (we use X_HELIDON_CN as the generated header for common name from client certificate)

@boris-senapt
Copy link
Author

@tomas-langer I understand that - but what endpoint-identification-algorithm does when enabled for client certificate authentication on the server side is that it tries to identify the client by its SAN. As a client certificate will identify a user and not a machine, and moreover the user's machine could have any IP address and likely not a domain name, this verification will always fail.

As an example, my WAN ip address might now be 1.2.3.4 - I have no reverse DNS for that IP as it's given to me by my ISP by DHCP. I make a request with my certificate for DN=CN=boris.morris provided for me by some public CA which issues client certificates. The server, if endpoint verification is enabled, will try and verify that there is an IP SAN for 1.2.3.4 in my certificate - which there obviously can never be.

Endpoint identification is used when the client talks to a server - which will have some fixed address(es) that can be named in a SAN.

For the server side of a mutual TLS connection, endpoint verification is normally disabled for this reason.

@m0mus m0mus added this to Backlog Aug 12, 2024
@m0mus m0mus moved this to Normal priority in Backlog Aug 12, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
4.x Version 4.x enhancement New feature or request P2 security
Projects
Status: Normal priority
Development

No branches or pull requests

3 participants