Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion docs/documentation/release_notes/topics/26_5_0.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ You can specify these headers via the `tracing-header-<header>` wildcard option,

For more details, see the link:{tracingguide_link}[{tracingguide_name}] guide.

= Sensitive Keycloak information is not displayed in the HTTP Access log

If you are using the HTTP Access logging capability, sensitive information is omitted.
It means that tokens in the 'Authorization' HTTP header and specific sensitive Keycloak cookies are not shown.

For more information, see https://www.keycloak.org/server/logging#http-access-logging[Configuring logging].

= Enable/disable features via a single option

You can now enable or disable individual features using the `feature-<name>` option (like `feature-spiffe=enabled`).
Expand All @@ -38,4 +45,4 @@ This provides a more fine-grained way to manage features and eliminates the need

The `feature-<name>` option takes precedence over both `features` and `features-disabled`.

For more details, see the https://www.keycloak.org/server/features[Enabling and disabling features] guide.
For more details, see the https://www.keycloak.org/server/features[Enabling and disabling features] guide.
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,10 @@ See the link:{upgradingguide_link}[{upgradingguide_name}] for details on how to

When enabling authorization on a new client (creating a Resource Server), Keycloak no longer automatically creates a "Default Resource", a "Default Policy," and a "Default Permission."

=== HTTP Access log does not contain specific sensitive information

Specific sensitive information is omitted from the HTTP Access log, such as the value of the `Authorization` HTTP header. Moreover, values of specific sensitive {project_name} cookies, such as `KEYCLOAK_SESSION`, `KEYCLOAK_IDENTITY`, or `AUTH_SESSION_ID`, are also omitted.

// ------------------------ Deprecated features ------------------------ //
== Deprecated features

Expand Down
2 changes: 1 addition & 1 deletion docs/guides/server/logging.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ You can even specify your own pattern with your required data to be logged, such
<@kc.start parameters="--http-access-log-pattern='%A %{METHOD} %{REQUEST_URL} %{i,User-Agent}'"/>

WARNING: HTTP Access logs may contain sensitive HTTP headers like `Authorization`, `Cookie`, or external API keys references.
Be careful with using the `long` pattern or printing the headers by the custom format - you should use it only for development purposes.
The `Authorization` header and specific sensitive {project_name} cookies are automatically omitted from the HTTP Access log.

Consult the https://quarkus.io/guides/http-reference#configuring-http-access-logs[Quarkus documentation] for the full list of variables that can be used.

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package org.keycloak.config;

import java.util.List;

public class HttpAccessLogOptions {

public static final Option<Boolean> HTTP_ACCESS_LOG_ENABLED = new OptionBuilder<>("http-access-log-enabled", Boolean.class)
Expand All @@ -20,4 +22,22 @@ public class HttpAccessLogOptions {
.category(OptionCategory.HTTP_ACCESS_LOG)
.description("A regular expression that can be used to exclude some paths from logging. For instance, '/realms/my-realm/.*' will exclude all subsequent endpoints for realm 'my-realm' from the log.")
.build();

// check the CookieType
public static final List<String> HIDDEN_COOKIE_VALUES = List.of(
"AUTH_SESSION_ID",
"KC_AUTH_SESSION_HASH",
"KEYCLOAK_IDENTITY",
"KEYCLOAK_SESSION",
"AUTH_SESSION_ID_LEGACY",
"KEYCLOAK_IDENTITY_LEGACY",
"KEYCLOAK_SESSION_LEGACY"
);

public static final Option<List<String>> HTTP_ACCESS_LOG_MASKED_COOKIES = OptionBuilder.listOptionBuilder("http-access-log-masked-cookies", String.class)
.category(OptionCategory.HTTP_ACCESS_LOG)
.description("Set of HTTP Cookie headers whose values must be masked when the 'long' pattern or '%{ALL_REQUEST_HEADERS}' format is enabled with the 'http-access-log-pattern' option.")
.hidden() // hidden for now as we do not have the full Quarkus support for this yet
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it matter if there's quarkus support whether this is hidden?

Will this ever be set by a user? If so, we could consider making it additive to the default.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was rather for purposes to be on a safe side, but yes, it might not be hidden.

It's expected that some additives will be there: #44433

.defaultValue(HIDDEN_COOKIE_VALUES)
.build();
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ public List<PropertyMapper<?>> getPropertyMappers() {
fromOption(HttpAccessLogOptions.HTTP_ACCESS_LOG_EXCLUDE)
.isEnabled(HttpAccessLogPropertyMappers::isHttpAccessLogEnabled, ACCESS_LOG_ENABLED_MSG)
.to("quarkus.http.access-log.exclude-pattern")
.build(),
fromOption(HttpAccessLogOptions.HTTP_ACCESS_LOG_MASKED_COOKIES)
.isEnabled(HttpAccessLogPropertyMappers::isHttpAccessLogEnabled, ACCESS_LOG_ENABLED_MSG)
.to("quarkus.http.access-log.masked-cookies") // not existing in current Quarkus version yet. Used in the CustomAllRequestHeadersAttribute
.build()
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package org.keycloak.quarkus.runtime.logging;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.StringJoiner;
import java.util.stream.Collectors;

import org.keycloak.quarkus.runtime.configuration.Configuration;

import io.quarkus.vertx.http.runtime.attribute.AllRequestHeadersAttribute;
import io.quarkus.vertx.http.runtime.attribute.ExchangeAttribute;
import io.quarkus.vertx.http.runtime.attribute.ExchangeAttributeBuilder;
import io.quarkus.vertx.http.runtime.attribute.ReadOnlyAttributeException;
import io.smallrye.config.ConfigValue;
import io.vertx.core.MultiMap;
import io.vertx.core.http.HttpHeaders;
import io.vertx.ext.web.RoutingContext;

// modified AllRequestHeadersAttribute class -> https://github.com/quarkusio/quarkus/blob/main/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/AllRequestHeadersAttribute.java
// remove once current Quarkus version includes the https://github.com/quarkusio/quarkus/pull/50672
public class CustomAllRequestHeadersAttribute implements ExchangeAttribute {

//---------------------Keycloak-changes-BEGIN----------------------------//
private static Set<String> getMaskedCookies(){
return Optional.ofNullable(Configuration.getConfigValue("quarkus.http.access-log.masked-cookies"))
.map(ConfigValue::getValue)
.map(f -> f.split(","))
.map(f -> new HashSet<>(Arrays.stream(f).toList()))
.orElseGet(HashSet::new);
}
//---------------------Keycloak-changes-END----------------------------//

private static final String AUTHORIZATION_HEADER = String.valueOf(HttpHeaders.AUTHORIZATION).toLowerCase();
private static final String COOKIE_HEADER = String.valueOf(HttpHeaders.COOKIE).toLowerCase();

private final Set<String> maskedHeaders;
private final Set<String> maskedCookies;

CustomAllRequestHeadersAttribute() {
this(Set.of(), Set.of());
}

CustomAllRequestHeadersAttribute(Set<String> maskedHeaders, Set<String> maskedCookies) {
this.maskedHeaders = toLowerCaseStringSet(maskedHeaders);
this.maskedCookies = toLowerCaseStringSet(maskedCookies);
}

private static Set<String> toLowerCaseStringSet(Set<String> set) {
return set.stream().map(String::toLowerCase).collect(Collectors.toSet());
}

@Override
public String readAttribute(RoutingContext exchange) {
return readAttribute(exchange.request().headers());
}

String readAttribute(MultiMap headers) {
if (headers.isEmpty()) {
return null;
} else {
final StringJoiner joiner = new StringJoiner(System.lineSeparator());

for (Map.Entry<String, String> header : headers) {
joiner.add(header.getKey() + ": " + maskHeaderValue(header.getKey(), header.getValue()));
}

return joiner.toString();
}
}

String maskHeaderValue(String headerName, String headerValue) {
if (headerValue == null) {
return null;
}

String headerNameLowerCase = headerName.toLowerCase();

if (AUTHORIZATION_HEADER.equals(headerNameLowerCase)) {
return maskAuthorizationHeaderValue(headerValue);
}

if (COOKIE_HEADER.equals(headerNameLowerCase)) {
return maskCookieHeaderValue(headerValue);
}

if (maskedHeaders.contains(headerNameLowerCase)) {
return "...";
}

return headerValue;
}

private String maskAuthorizationHeaderValue(String headerValue) {
int idx = headerValue.indexOf(' ');
final String scheme = idx > 0 ? headerValue.substring(0, idx) : null;

if (scheme != null) {
return scheme + " ...";
} else {
return "...";
}
}

private String maskCookieHeaderValue(String headerValue) {
int idx = headerValue.indexOf('=');

final String cookieName = idx > 0 ? headerValue.substring(0, idx) : null;

if (cookieName != null && maskedCookies.contains(cookieName.toLowerCase())) {
return cookieName + "=...";
}

return headerValue;
}

@Override
public void writeAttribute(RoutingContext exchange, String newValue) throws ReadOnlyAttributeException {
throw new ReadOnlyAttributeException("Headers", newValue);
}

public static final class Builder implements ExchangeAttributeBuilder {

@Override
public String name() {
return "Headers";
}

@Override
public ExchangeAttribute build(final String token) {
if (token.equals("%{ALL_REQUEST_HEADERS}")) {
//---------------------Keycloak-changes-BEGIN----------------------------//
return new CustomAllRequestHeadersAttribute(Set.of(), getMaskedCookies());
//---------------------Keycloak-changes-END----------------------------//
}
return null;
}

@Override
public int priority() {
//---------------------Keycloak-changes-BEGIN----------------------------//
// increase the priority
return new AllRequestHeadersAttribute.Builder().priority() + 1;
//---------------------Keycloak-changes-END----------------------------//
}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
org.keycloak.quarkus.runtime.logging.CustomAllRequestHeadersAttribute$Builder
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@
import java.nio.file.Path;
import java.nio.file.Paths;

import org.keycloak.config.HttpAccessLogOptions;
import org.keycloak.config.LoggingOptions;
import org.keycloak.connections.httpclient.HttpClientBuilder;
import org.keycloak.cookie.CookieType;
import org.keycloak.it.junit5.extension.CLIResult;
import org.keycloak.it.junit5.extension.DistributionTest;
import org.keycloak.it.junit5.extension.DryRun;
Expand All @@ -34,10 +37,16 @@

import io.quarkus.deployment.util.FileUtil;
import io.quarkus.test.junit.main.Launch;
import org.apache.http.HttpHeaders;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.RequestBuilder;
import org.apache.http.util.EntityUtils;
import org.hamcrest.CoreMatchers;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.testcontainers.shaded.org.apache.commons.io.FileUtils;

import static org.keycloak.OAuth2Constants.DPOP_HTTP_HEADER;
import static org.keycloak.quarkus.runtime.cli.command.Main.CONFIG_FILE_LONG_NAME;

import static io.restassured.RestAssured.when;
Expand Down Expand Up @@ -303,4 +312,58 @@ void httpAccessLogNotNamedPattern(CLIResult cliResult) {
.statusCode(200);
cliResult.assertNoMessage("http://127.0.0.1:8080/realms/master/clients/account/redirect");
}

@Test
@Launch({"start-dev", "--http-access-log-enabled=true", "--http-access-log-pattern=long"})
void httpAccessLogMaskedCookies(CLIResult cliResult) {
assertHttpAccessLogMaskedCookies(cliResult);
}

@Test
@Launch({"start-dev", "--http-access-log-enabled=true", "--http-access-log-pattern='%{ALL_REQUEST_HEADERS}'"})
void httpAccessLogMaskedCookiesDiffFormat(CLIResult cliResult) {
assertHttpAccessLogMaskedCookies(cliResult);
}

private void assertHttpAccessLogMaskedCookies(CLIResult cliResult) {
assertThat(HttpAccessLogOptions.HIDDEN_COOKIE_VALUES.contains(CookieType.AUTH_SESSION_ID.getName()), CoreMatchers.is(true));
assertThat(HttpAccessLogOptions.HIDDEN_COOKIE_VALUES.contains(CookieType.AUTH_SESSION_ID_HASH.getName()), CoreMatchers.is(true));
assertThat(HttpAccessLogOptions.HIDDEN_COOKIE_VALUES.contains(CookieType.IDENTITY.getName()), CoreMatchers.is(true));
assertThat(HttpAccessLogOptions.HIDDEN_COOKIE_VALUES.contains(CookieType.SESSION.getName()), CoreMatchers.is(true));

cliResult.assertStartedDevMode();

try (var httpClient = new HttpClientBuilder().build()) {
var baseRequest = RequestBuilder.post().setUri("http://localhost:8080/realms/master");

var sensitiveCookiesRequest = baseRequest.addHeader(HttpHeaders.AUTHORIZATION, "Bearer something-that-should-be-hidden");
HttpAccessLogOptions.HIDDEN_COOKIE_VALUES.forEach(cookie -> sensitiveCookiesRequest.addHeader("Cookie", cookie + "=something-that-should-be-hidden"));

try (CloseableHttpResponse response = httpClient.execute(sensitiveCookiesRequest.build())) {
assertThat(response, notNullValue());
EntityUtils.consumeQuietly(response.getEntity());
}

var differentAuthorizationHeader = baseRequest
.addHeader(HttpHeaders.AUTHORIZATION, DPOP_HTTP_HEADER + " something-that-should-be-hidden")
.addHeader(HttpHeaders.CONTENT_LANGUAGE, "cs")
.addHeader("Cookie", "SOMETHING=something-not-sensitive")
.build();

try (CloseableHttpResponse response = httpClient.execute(differentAuthorizationHeader)) {
assertThat(response, notNullValue());
EntityUtils.consumeQuietly(response.getEntity());
}
} catch (IOException e) {
throw new RuntimeException(e);
}

// Verify that sensitive cookie values are masked in the access log
cliResult.assertMessage("[org.keycloak.http.access-log]");
cliResult.assertMessage("Authorization: Bearer ...");
cliResult.assertMessage("Authorization: DPoP ...");
cliResult.assertMessage("Cookie: SOMETHING=something-not-sensitive");
cliResult.assertMessage("Content-Language: cs");
HttpAccessLogOptions.HIDDEN_COOKIE_VALUES.forEach(cookie -> cliResult.assertMessage("Cookie: %s=...".formatted(cookie)));
}
}
Loading