Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2022, 2023 Contributors to the Eclipse Foundation. All rights reserved.
* Copyright (c) 2022, 2026 Contributors to the Eclipse Foundation. All rights reserved.
* Copyright (c) 1997, 2020 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
Expand All @@ -17,9 +17,11 @@

package org.glassfish.admingui.common.security;

import com.sun.enterprise.config.serverbeans.Config;
import com.sun.enterprise.config.serverbeans.Domain;
import com.sun.enterprise.config.serverbeans.SecureAdmin;
import com.sun.enterprise.security.SecurityServicesUtil;
import com.sun.enterprise.util.net.NetUtils;

import jakarta.security.auth.message.AuthException;
import jakarta.security.auth.message.AuthStatus;
Expand Down Expand Up @@ -56,6 +58,7 @@
import org.glassfish.admingui.common.util.RestUtil;
import org.glassfish.common.util.InputValidationUtil;
import org.glassfish.grizzly.config.dom.NetworkListener;
import org.glassfish.grizzly.config.dom.Protocol;
import org.glassfish.hk2.api.ServiceLocator;
import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature;

Expand Down Expand Up @@ -120,10 +123,8 @@ public void initialize(MessagePolicy requestPolicy, MessagePolicy responsePolicy
throw new AuthException(
"'loginErrorPage' " + "must be supplied as a property in the provider-config " + "in the domain.xml file!");
}
ServiceLocator habitat = SecurityServicesUtil.getInstance().getHabitat();
Domain domain = habitat.getService(Domain.class);
NetworkListener adminListener = domain.getServerNamed("server").getConfig().getNetworkConfig()
.getNetworkListener("admin-listener");
ServiceLocator habitat = getServiceLocator();
NetworkListener adminListener = getAdminListener();
SecureAdmin secureAdmin = habitat.getService(SecureAdmin.class);

final String host = adminListener.getAddress();
Expand All @@ -133,6 +134,16 @@ public void initialize(MessagePolicy requestPolicy, MessagePolicy responsePolicy
}
}

private static ServiceLocator getServiceLocator() {
return SecurityServicesUtil.getInstance().getHabitat();
}

private static NetworkListener getAdminListener() {
Config config = getServiceLocator().getService(Domain.class)
.getServerNamed("server").getConfig();
return config.getAdminListener();
}

/**
*
*/
Expand Down Expand Up @@ -206,10 +217,15 @@ public AuthStatus validateRequest(MessageInfo messageInfo, Subject clientSubject
Client client2 = RestUtil.initialize(ClientBuilder.newBuilder()).build();
WebTarget target = client2.target(restURL);
target.register(HttpAuthenticationFeature.basic(username, new String(password)));

// Get the real remote host, checking for proxy headers if behind a reverse proxy
String remoteHost = getRemoteHost(request);
MultivaluedMap payLoad = new MultivaluedHashMap();
payLoad.putSingle("remoteHostName", request.getRemoteHost());
payLoad.putSingle("remoteHostName", remoteHost);

Response resp = target.request(RESPONSE_TYPE).post(Entity.entity(payLoad, MediaType.APPLICATION_FORM_URLENCODED), Response.class);
Response resp = target.request(RESPONSE_TYPE)
.header("X-GlassFish-Remote-Host", remoteHost)
.post(Entity.entity(payLoad, MediaType.APPLICATION_FORM_URLENCODED), Response.class);
RestResponse restResp = RestResponse.getRestResponse(resp);
Arrays.fill(password, ' ');
// Check to see if successful..
Expand Down Expand Up @@ -270,7 +286,11 @@ public AuthStatus validateRequest(MessageInfo messageInfo, Subject clientSubject
return AuthStatus.SEND_CONTINUE;
} else {
int status = restResp.getResponseCode();
if (status == 403) {
if (status == 429) {
// Too many concurrent requests - rate limited
request.setAttribute("errorText", GuiUtil.getMessage("alert.AuthenticationFailed"));
request.setAttribute("messageText", GuiUtil.getMessage("alert.TryAgainLater"));
} else if (status == 403) {
request.setAttribute("errorText", GuiUtil.getMessage("alert.ConfigurationError"));
request.setAttribute("messageText", GuiUtil.getMessage("alert.EnableSecureAdmin"));
}
Expand Down Expand Up @@ -301,4 +321,31 @@ public void cleanSubject(MessageInfo messageInfo, Subject subject) throws AuthEx
private boolean isMandatory(MessageInfo messageInfo) {
return Boolean.valueOf((String) messageInfo.getMap().get("jakarta.security.auth.message.MessagePolicy.isMandatory"));
}

/**
* Gets the real remote host, checking for proxy headers if behind a reverse proxy.
* Only uses proxy headers when behindProxy is enabled in configuration.
*/
private String getRemoteHost(HttpServletRequest request) {
// Check if behind proxy is enabled
Protocol protocol = getAdminListener().findHttpProtocol();
boolean behindProxy = protocol != null && protocol.getHttp() != null
&& protocol.getHttp().isBehindProxy();

return NetUtils.getRemoteHost(
new NetUtils.RequestInfoProvider() {
@Override
public String getHeader(String name) {
return request.getHeader(name);
}

@Override
public String getRemoteHost() {
return request.getRemoteHost();
}

},
behindProxy
);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#
# Copyright (c) 2023 Contributors to the Eclipse Foundation.
# Copyright (c) 2023, 2026 Contributors to the Eclipse Foundation.
# Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved.
#
# This program and the accompanying materials are made available under the
Expand Down Expand Up @@ -1016,6 +1016,7 @@ alert.AuthenticationFailed=Authentication Failed
alert.ReenterUsernamePassword=Re-enter your username and password
alert.ConfigurationError=Configuration Error
alert.EnableSecureAdmin=Secure Admin must be enabled to access the DAS remotely.
alert.TryAgainLater=Authentication refused, try again later.

## Recover Transactions
headings.recoverTransactions=Recover Transactions
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<!--

Copyright (c) 2022, 2026 Contributors to the Eclipse Foundation
Copyright (c) 2018 Oracle and/or its affiliates. All rights reserved.

This program and the accompanying materials are made available under the
Expand Down Expand Up @@ -56,7 +57,7 @@
setPageSessionAttribute(key="valueMap" value="#{pageSession.httpMap}");
setPageSessionAttribute(key="convertToFalseList"
value={"uploadTimeoutEnabled", "cometSupportEnabled", "dnsLookupEnabled", "rcmSupportEnabled", "traceEnabled", "authPassThroughEnabled",
"chunkingEnabled", "encodedSlashEnabled", "websocketsSupport", "xpoweredBy" });
"chunkingEnabled", "encodedSlashEnabled", "websocketsSupport", "xpoweredBy", "behindProxy" });

//set the following for including buttons.inc

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@
<sun:textField id="ServerName" columns="$int{50}" maxLength="#{sessionScope.fieldLengths['maxLength.http.serverName']}" text="#{pageSession.httpMap['serverName']}" />
</sun:property>

<sun:property id="behindProxy" labelAlign="left" noWrap="#{true}" overlapLabel="#{false}" label="$resource{i18n_web.http.BehindProxy}" helpText="$resource{i18n_web.http.BehindProxyHelp}" >
<sun:checkbox id="behindProxyEnabled" label="$resource{i18n_web.common.enabled}" selected="#{pageSession.httpMap['behindProxy']}" selectedValue="true" />
</sun:property>

<sun:property id="DefaultVirtServersProp" labelAlign="left" noWrap="#{true}" overlapLabel="#{false}" label="$resource{i18n_web.http.defVirtualServerLabel}" helpText="$resource{i18n_web.http.defVirtualServerLabelHelp}">
<sun:dropDown id="vs" selected="#{pageSession.httpMap['defaultVirtualServer']}" labels="$pageSession{vsList}" values="$pageSession{vsList}" />
</sun:property>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,8 @@ http.EncodedSlash=Encoded Slash:
http.EncodedSlashHelp=Allow encoded slash in URIs
http.WebsocketsSupport=Websockets Support:
http.WebsocketsSupportHelp=
http.BehindProxy=Behind Proxy:
http.BehindProxyHelp=Enable when GlassFish is behind a reverse proxy to read client IP from X-Real-IP and X-Forwarded-For headers
http.MaxSavePostSize=Max Save Post Size
http.MaxSavePostSizeHelp=Maximum size of a POST which will be saved by the container during authentication.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@

import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import static org.junit.jupiter.api.condition.OS.WINDOWS;

Expand Down
22 changes: 22 additions & 0 deletions docs/administration-guide/src/main/asciidoc/http_https.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,7 @@ The following topics are addressed here:

* <<To Create an HTTP Configuration>>
* <<To Delete an HTTP Configuration>>
* <<Behind Proxy Configuration>>

[[to-create-an-http-configuration]]

Expand Down Expand Up @@ -382,6 +383,27 @@ See Also
You can also view the full syntax and options of the subcommand by
typing `asadmin help delete-http` at the command line.

[[behind-proxy-configuration]]

===== Behind Proxy Configuration

When GlassFish Server is deployed behind a reverse proxy (such as nginx or Apache HTTP Server), you can enable the `behind-proxy` option to have the server read client IP addresses from proxy headers instead of the direct connection's remote address.

To enable behind-proxy mode, use the `asadmin set` command:

[source]
----
asadmin set configs.config.server-config.network-config.protocols.protocol.admin-listener.http.behind-proxy=true
----

When `behind-proxy` is enabled:

* The server reads the `X-Real-IP` header first (set by the closest proxy)
* If `X-Real-IP` is not present, it reads the `X-Forwarded-For` header and takes the first IP in the list (original client)
* If neither header is present, it falls back to the request's direct remote address

This configuration is useful for ensuring proper client IP tracking for brute-force attack protection.

[[administering-http-transports]]

==== Administering HTTP Transports
Expand Down
24 changes: 24 additions & 0 deletions docs/security-guide/src/main/asciidoc/system-security.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1066,6 +1066,7 @@ The following topics are addressed here:
`start-cluster` Subcommands]
* xref:#using-start-instance-and-start-cluster-with-a-password-file[Using `start-instance` and `start-cluster` With a Password File]
* xref:#to-change-an-administration-password[To Change an Administration Password]
* xref:#brute-force-attack-protection[Brute-Force Attack Protection]
* xref:#to-set-a-password-from-a-file[To Set a Password From a File]
* xref:#administering-password-aliases[Administering Password Aliases]

Expand Down Expand Up @@ -1335,6 +1336,29 @@ See Also
You can also view the full syntax and options of the subcommand by
typing `asadmin help change-admin-password` at the command line.


[[brute-force-attack-protection]]
==== Brute-Force Attack Protection

{productName} includes protection against brute-force authentication attacks on the administration interface (both the Administration Console and REST API). This protection works by:

* Tracking failed authentication attempts per username and remote host combination
* Applying an exponential delay after each failed attempt (1 second, 2 seconds, 4 seconds, 8 seconds, etc.)
* Capping the maximum delay at 60 seconds
* Rejecting additional concurrent authentication attempts when too many requests are being delayed for the same user/host (HTTP 429 Too Many Requests)

This mechanism makes it impractical for attackers to try many passwords in rapid succession while allowing legitimate users to retry after a short wait.

This mechanism applies only for remote connections. Local connections are never delayed.

[[behind-proxy-configuration]]

##### Behind Proxy Configuration

When the server is deployed behind a reverse proxy, you must configure GlassFish to trust the proxy headers (`X-Real-IP` and `X-Forwarded-For`) to correctly identify the client's real IP address. Without this configuration, all authentication attempts would be tracked under the proxy's IP address instead of individual client IPs. Any failed authentication attempt would delay authentication for all clients, even a valid client with the correct credentials.

For more information about configuring behind-proxy mode and other HTTP options, see xref:administration-guide.adoc#behind-proxy-configuration[Behind Proxy Configuration] in the Administration Guide.

[[to-set-a-password-from-a-file]]

==== To Set a Password From a File
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,10 @@ public static Object getTranslatedValue(Object value) {
if (stringValue.indexOf('$') == -1) {
return value;
}
DomainScopedPasswordAliasStore dasPasswordAliasStore = domainPasswordAliasStore();
if (dasPasswordAliasStore != null) {
if (getAlias(stringValue) != null) {
if (getAlias(stringValue) != null) {
// First search for alias in value, it's faster than loading alias store. If no alias in value, store doesn't need to load. Speeds up startup.
DomainScopedPasswordAliasStore dasPasswordAliasStore = domainPasswordAliasStore();
if (dasPasswordAliasStore != null) {
try {
return getRealPasswordFromAlias(stringValue, dasPasswordAliasStore);
} catch (Exception e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

package org.glassfish.admin.rest.adapter;

import com.sun.enterprise.admin.util.AuthenticationAttemptTracker;
import com.sun.enterprise.config.serverbeans.Config;
import com.sun.enterprise.util.LocalStringManagerImpl;

Expand Down Expand Up @@ -75,7 +76,6 @@

import static java.lang.System.Logger.Level.DEBUG;
import static java.lang.System.Logger.Level.INFO;
import static java.lang.System.Logger.Level.WARNING;
import static java.nio.charset.StandardCharsets.UTF_8;

/**
Expand Down Expand Up @@ -177,9 +177,18 @@ public void service(Request req, Response res) {
reportError(req, res, HttpURLConnection.HTTP_FORBIDDEN, localStrings.getLocalString("rest.adapter.auth.forbidden",
"Remote access not allowed. If you desire remote access, please turn on secure admin"), e);
} catch (LoginException e) {
int status = HttpURLConnection.HTTP_UNAUTHORIZED;
String msg = localStrings.getLocalString("rest.adapter.auth.userpassword", "Invalid user name or password");
res.setHeader(HEADER_AUTHENTICATE, "BASIC");
int status;
String msg;

if (e instanceof AuthenticationAttemptTracker.TooManyRequestsException) {
status = 429; // HTTP_TOO_MANY_REQUESTS
msg = e.getMessage();
} else {
status = HttpURLConnection.HTTP_UNAUTHORIZED;
msg = localStrings.getLocalString("rest.adapter.auth.userpassword", "Invalid user name or password");
res.setHeader(HEADER_AUTHENTICATE, "BASIC");
}

reportError(req, res, status, msg, e);
} catch (Exception e) {
// TODO: This string is duplicated. Can we pull this text out of the logging bundle?
Expand Down Expand Up @@ -302,7 +311,7 @@ private JerseyContainer getJerseyContainer(ResourceConfig config) {
}

private void reportError(Request req, Response res, int statusCode, String msg, Exception exception) {
LOG.log(WARNING, "reportError(req, res, statusCode=" + statusCode + ", msg, e)", exception);
LOG.log(DEBUG, "reportError(req, res, statusCode=" + statusCode + ", msg, e)", exception);
try {
// TODO: There's a lot of arm waving and failing here. I'd like this to be cleaner, but I don't
// have time at the moment. jdlee 8/11/10
Expand Down
Loading
Loading