Skip to content

Commit

Permalink
Remove basic auth from datanode rest api (jwt only now) (#19244)
Browse files Browse the repository at this point in the history
* Remove basic auth from datanode rest api (jwt only now)

* Remove username/password usages from datanode integration tests

* fix tests

* Added changelog
  • Loading branch information
todvora committed May 6, 2024
1 parent 3eaef55 commit e098d67
Show file tree
Hide file tree
Showing 13 changed files with 120 additions and 286 deletions.
5 changes: 5 additions & 0 deletions changelog/unreleased/issue-19243.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
type="f"
message="Removed basic auth for datanode rest api. Only JWT supported now."

issues=["19243"]
pulls=["19244"]
21 changes: 0 additions & 21 deletions data-node/src/main/java/org/graylog/datanode/Configuration.java
Original file line number Diff line number Diff line change
Expand Up @@ -158,14 +158,6 @@ public class Configuration {
@Parameter(value = HTTP_CERTIFICATE_PASSWORD_PROPERTY)
private String datanodeHttpCertificatePassword;

@Documentation("You MUST specify a hash password for the root user (which you only need to initially set up the " +
"system and in case you lose connectivity to your authentication backend)." +
"This password cannot be changed using the API or via the web interface. If you need to change it, " +
"modify it in this file. " +
"Create one by using for example: echo -n yourpassword | shasum -a 256")
@Parameter(value = "root_password_sha2")
private String rootPasswordSha2;

@Documentation("You MUST set a secret to secure/pepper the stored user passwords here. Use at least 64 characters." +
"Generate one by using for example: pwgen -N 1 -s 96 \n" +
"ATTENTION: This value must be the same on all Graylog and Datanode nodes in the cluster. " +
Expand All @@ -186,10 +178,6 @@ public class Configuration {
@Parameter(value = "node_id_file", validators = NodeIdFileValidator.class)
private String nodeIdFile = "data/node-id";

@Documentation("The default root user is named 'admin'")
@Parameter(value = "root_username")
private String rootUsername = "admin";

@Documentation("HTTP bind address. The network interface used by the Graylog DataNode to bind all services.")
@Parameter(value = "bind_address", required = true)
private String bindAddress = DEFAULT_BIND_ADDRESS;
Expand Down Expand Up @@ -387,10 +375,6 @@ public void validatePasswordSecret() throws ValidationException {
}
}

public String getRootUsername() {
return rootUsername;
}

public String getDatanodeNodeName() {
return datanodeNodeName != null && !datanodeNodeName.isBlank() ? datanodeNodeName : getHostname();
}
Expand Down Expand Up @@ -653,11 +637,6 @@ public String getHostname() {
return bindAddress;
}

public String getRootPasswordSha2() {
return rootPasswordSha2;
}


public String getNodeSearchCacheSize() {
return searchCacheSize;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,8 @@
import com.google.common.net.HostAndPort;
import com.google.common.util.concurrent.AbstractIdleService;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import jakarta.annotation.Nonnull;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import jakarta.ws.rs.container.ContainerRequestFilter;
import jakarta.ws.rs.container.DynamicFeature;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.ext.ContextResolver;
Expand All @@ -52,7 +50,6 @@
import org.graylog.datanode.opensearch.configuration.OpensearchConfiguration;
import org.graylog.datanode.rest.config.SecuredNodeAnnotationFilter;
import org.graylog.security.certutil.CertConstants;
import org.graylog2.bootstrap.preflight.web.BasicAuthFilter;
import org.graylog2.configuration.TLSProtocolsConfiguration;
import org.graylog2.plugin.inject.Graylog2Module;
import org.graylog2.rest.MoreMediaTypes;
Expand Down Expand Up @@ -235,7 +232,7 @@ private HttpServer setUp(URI listenUri,
final ResourceConfig resourceConfig = buildResourceConfig(additionalResources);

if (isSecuredInstance) {
resourceConfig.register(createAuthFilter(configuration));
resourceConfig.register(new JwtTokenAuthFilter(configuration.getPasswordSecret()));
}
resourceConfig.register(new SecuredNodeAnnotationFilter(configuration.isInsecureStartup()));

Expand Down Expand Up @@ -264,13 +261,6 @@ private HttpServer setUp(URI listenUri,
return httpServer;
}

@Nonnull
private ContainerRequestFilter createAuthFilter(Configuration configuration) {
final ContainerRequestFilter basicAuthFilter = new BasicAuthFilter(configuration.getRootUsername(), configuration.getRootPasswordSha2(), "Datanode");
final AuthTokenValidator tokenVerifier = new JwtTokenValidator(configuration.getPasswordSecret());
return new DatanodeAuthFilter(basicAuthFilter, tokenVerifier);
}

private SSLEngineConfigurator buildSslEngineConfigurator(KeystoreInformation keystoreInformation)
throws GeneralSecurityException, IOException {
if (keystoreInformation == null || !Files.isRegularFile(keystoreInformation.location()) || !Files.isReadable(keystoreInformation.location())) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@
*/
package org.graylog.datanode.initializers;

import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.UnsupportedJwtException;
import io.jsonwebtoken.security.Keys;
import jakarta.inject.Named;
import jakarta.inject.Singleton;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerRequestFilter;
import jakarta.ws.rs.core.HttpHeaders;
Expand All @@ -25,53 +31,67 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.crypto.SecretKey;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Optional;

/**
* This is an authorization filter that first try to verify presence and validity of a bearer token. If there is no
* bearer token available, it will fallback to basic auth (or whatever filter is configured as fallback).
* Allowing both auth methods allows easy access directly from CLI or browser and machine-machine communication from the graylog server.
*/
public class DatanodeAuthFilter implements ContainerRequestFilter {

private static final Logger LOG = LoggerFactory.getLogger(DatanodeAuthFilter.class);
private static final String AUTHENTICATION_SCHEME = "Bearer";
private final ContainerRequestFilter fallbackFilter;
private final AuthTokenValidator tokenVerifier;
@Singleton
public class JwtTokenAuthFilter implements ContainerRequestFilter {

private static final Logger LOG = LoggerFactory.getLogger(JwtTokenAuthFilter.class);

public DatanodeAuthFilter(ContainerRequestFilter fallbackFilter, AuthTokenValidator tokenVerifier) {
this.fallbackFilter = fallbackFilter;
this.tokenVerifier = tokenVerifier;
}
private static final String AUTHENTICATION_SCHEME = "Bearer";
public static final String REQUIRED_SUBJECT = "admin";
public static final String REQUIRED_ISSUER = "graylog";
private final String signingKey;

private Optional<String> getBearerHeader(ContainerRequestContext requestContext) {
final MultivaluedMap<String, String> headers = requestContext.getHeaders();
return headers.getOrDefault(HttpHeaders.AUTHORIZATION, Collections.emptyList())
.stream()
.filter(a -> a.startsWith(AUTHENTICATION_SCHEME))
.findFirst();
public JwtTokenAuthFilter(@Named("password_secret") final String signingKey) {
this.signingKey = signingKey;
}

@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
final Optional<String> header = getBearerHeader(requestContext);
if (header.isEmpty()) {
// no JWT token, we'll fallback to basic auth
fallbackFilter.filter(requestContext);
// no JWT token, we'll fail immediately
abortRequest(requestContext);
} else {
final String token = header.map(h -> h.replaceFirst(AUTHENTICATION_SCHEME + " ", "")).get();
try {
tokenVerifier.verifyToken(token);
verifyToken(token);
} catch (TokenVerificationException e) {
LOG.error("Failed to verify auth token", e);
abortRequest(requestContext);
}
}
}

private Optional<String> getBearerHeader(ContainerRequestContext requestContext) {
final MultivaluedMap<String, String> headers = requestContext.getHeaders();
return headers.getOrDefault(HttpHeaders.AUTHORIZATION, Collections.emptyList())
.stream()
.filter(a -> a.startsWith(AUTHENTICATION_SCHEME))
.findFirst();
}

void verifyToken(String token) throws TokenVerificationException {
final SecretKey key = Keys.hmacShaKeyFor(this.signingKey.getBytes(StandardCharsets.UTF_8));
final JwtParser parser = Jwts.parser()
.verifyWith(key)
.requireSubject(REQUIRED_SUBJECT)
.requireIssuer(REQUIRED_ISSUER)
.build();
try {
parser.parse(token);
} catch (UnsupportedJwtException e) {
throw new TokenVerificationException("Token format/configuration is not supported", e);
} catch (Throwable e) {
throw new TokenVerificationException(e);
}
}


private void abortRequest(ContainerRequestContext requestContext) {
requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED)
Expand Down

This file was deleted.

This file was deleted.

Loading

0 comments on commit e098d67

Please sign in to comment.