Selenium Grid is a distributed testing infrastructure that allows running WebDriver tests in parallel across multiple machines and browsers.
—
Selenium Grid provides authentication and authorization mechanisms to secure grid endpoints, manage access control, and protect sensitive operations through secret management and request filtering.
Core secret handling for authentication and authorization across grid components.
/**
* Wrapper for sensitive values like passwords and API keys
*/
class Secret {
// Implementation intentionally opaque for security
/** Create a secret from a string value */
static Secret fromString(String value);
/** Create a secret from environment variable */
static Secret fromEnvironment(String envVar);
/** Create a secret from file contents */
static Secret fromFile(Path filePath) throws IOException;
/** Check if this secret matches another secret */
boolean matches(Secret other);
/** Check if this secret matches a string value */
boolean matches(String value);
// Note: No methods to retrieve the actual secret value
// This prevents accidental logging or exposure
}Usage Example:
// Create secrets from various sources
Secret registrationSecret = Secret.fromEnvironment("SE_REGISTRATION_SECRET");
Secret apiKey = Secret.fromFile(Paths.get("/etc/selenium/api-key"));
Secret password = Secret.fromString("my-secure-password");
// Use secrets for authentication
Node node = LocalNode.builder(tracer, eventBus, nodeUri, gridUri, registrationSecret)
.build();
// Secrets automatically used in HTTP headers for authentication
// Grid components validate secrets before processing requestsHTTP Basic Authentication support for protecting grid endpoints.
/**
* HTTP Basic Authentication filter for grid endpoints
*/
class BasicAuthenticationFilter implements Filter {
/** Create filter with username and password secret */
BasicAuthenticationFilter(String username, Secret password);
/** Create filter from configuration */
static BasicAuthenticationFilter create(Config config);
@Override
void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException;
}Configuration Example:
[server]
enable-basic-auth = true
username = "admin"
password = "secret_password"
[server.auth]
realm = "Selenium Grid"Filter that validates request secrets for API access control.
/**
* Filter that validates secret tokens in request headers
*/
class RequiresSecretFilter implements Filter {
/** Create filter with required secret */
RequiresSecretFilter(Secret requiredSecret);
/** Create filter from configuration */
static RequiresSecretFilter create(Config config);
@Override
void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException;
/** Get the expected header name for the secret */
static String getSecretHeaderName();
}Usage Example:
// Configure secret validation
Secret apiSecret = Secret.fromEnvironment("SE_API_SECRET");
RequiresSecretFilter secretFilter = new RequiresSecretFilter(apiSecret);
// Apply to specific endpoints
server.addFilter(secretFilter, "/grid/api/*");
// Clients must include secret in headers
// X-Registration-Secret: <secret-value>Security-specific configuration settings.
/**
* Configuration options for security features
*/
class SecretOptions {
static final String SECRET_SECTION = "secret";
/** Get registration secret for node authentication */
Secret getRegistrationSecret(Config config);
/** Get API access secret */
Secret getApiSecret(Config config);
/** Get whether to require secrets for node registration */
boolean requireRegistrationSecret(Config config);
/** Get whether to require secrets for API access */
boolean requireApiSecret(Config config);
}
/**
* Security-related command-line flags
*/
class SecurityFlags {
@Parameter(names = {"--registration-secret"},
description = "Secret required for node registration")
String registrationSecret;
@Parameter(names = {"--api-secret"},
description = "Secret required for API access")
String apiSecret;
@Parameter(names = {"--require-registration-secret"},
description = "Require secret for node registration")
boolean requireRegistrationSecret = false;
@Parameter(names = {"--enable-basic-auth"},
description = "Enable HTTP Basic Authentication")
boolean enableBasicAuth = false;
@Parameter(names = {"--basic-auth-username"},
description = "Username for Basic Authentication")
String basicAuthUsername = "admin";
@Parameter(names = {"--basic-auth-password"},
description = "Password for Basic Authentication")
String basicAuthPassword;
}// Secure node registration with shared secret
public class SecureNodeRegistration {
public static void registerNode() {
// Node includes registration secret
Secret registrationSecret = Secret.fromEnvironment("SE_REGISTRATION_SECRET");
Node node = LocalNode.builder(tracer, eventBus, nodeUri, gridUri, registrationSecret)
.add(chromeCapabilities, chromeFactory)
.build();
// Grid validates secret before accepting registration
// If secret doesn't match, registration is rejected
}
// Grid side validation
public boolean validateNodeRegistration(HttpRequest request, Secret expectedSecret) {
String secretHeader = request.getHeader("X-Registration-Secret");
if (secretHeader == null) {
return !requireRegistrationSecret; // Allow if not required
}
return expectedSecret.matches(secretHeader);
}
}// Protect sensitive grid operations
@Path("/grid/admin")
public class AdminEndpoints {
@POST
@Path("/shutdown")
@RequiresSecret
public Response shutdownGrid() {
// Only accessible with valid API secret
gridManager.shutdown();
return Response.ok().build();
}
@DELETE
@Path("/sessions")
@RequiresSecret
public Response clearAllSessions() {
// Requires API secret to clear all sessions
sessionMap.clear();
return Response.ok().build();
}
}
// Custom annotation for secret requirement
@Target({METHOD, TYPE})
@Retention(RUNTIME)
public @interface RequiresSecret {
}// Configure HTTPS for grid communications
public class TLSConfiguration {
public static Server createSecureServer(Config config) {
ServerOptions serverOptions = new ServerOptions(config);
if (serverOptions.isTlsEnabled()) {
return new NettyServer(
serverOptions,
HttpHandler.combine(
new TlsHandler(
serverOptions.getKeyStore(),
serverOptions.getKeyStorePassword(),
serverOptions.getTrustStore()
),
routeHandlers
)
);
}
return new NettyServer(serverOptions, routeHandlers);
}
}TLS Configuration Example:
[server]
https = true
keystore = "/etc/selenium/keystore.jks"
keystore-password = "keystore_password"
truststore = "/etc/selenium/truststore.jks"// Different access levels for different operations
public enum GridRole {
ADMIN("admin"), // Full access to all operations
OPERATOR("operator"), // Can manage sessions and nodes
VIEWER("viewer"); // Read-only access
private final String roleName;
GridRole(String roleName) {
this.roleName = roleName;
}
}
public class RoleBasedAuthFilter implements Filter {
private final Map<String, Set<GridRole>> endpointRoles;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
String path = ((HttpServletRequest) request).getPathInfo();
Set<GridRole> requiredRoles = endpointRoles.get(path);
if (requiredRoles != null && !hasRequiredRole(request, requiredRoles)) {
((HttpServletResponse) response).setStatus(403);
return;
}
chain.doFilter(request, response);
}
private boolean hasRequiredRole(ServletRequest request, Set<GridRole> required) {
// Extract user roles from token/certificate/etc.
Set<GridRole> userRoles = extractUserRoles(request);
return userRoles.stream().anyMatch(required::contains);
}
}// Restrict access by client IP address
public class IPWhitelistFilter implements Filter {
private final Set<String> allowedIPs;
private final Set<String> allowedSubnets;
public IPWhitelistFilter(Config config) {
this.allowedIPs = Set.of(config.get("security", "allowed-ips").orElse("").split(","));
this.allowedSubnets = Set.of(config.get("security", "allowed-subnets").orElse("").split(","));
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
String clientIP = request.getRemoteAddr();
if (!isAllowedIP(clientIP)) {
((HttpServletResponse) response).setStatus(403);
return;
}
chain.doFilter(request, response);
}
private boolean isAllowedIP(String clientIP) {
return allowedIPs.contains(clientIP) ||
allowedSubnets.stream().anyMatch(subnet -> isInSubnet(clientIP, subnet));
}
}// Support for rotating secrets without downtime
public class RotatingSecretManager {
private final List<Secret> validSecrets; // Current + previous secrets
private final ScheduledExecutorService scheduler;
public boolean validateSecret(String providedSecret) {
return validSecrets.stream()
.anyMatch(secret -> secret.matches(providedSecret));
}
public void rotateSecret(Secret newSecret) {
// Add new secret while keeping old one valid
validSecrets.add(0, newSecret); // New secret first
// Schedule removal of old secret after grace period
scheduler.schedule(() -> {
if (validSecrets.size() > 1) {
validSecrets.remove(validSecrets.size() - 1); // Remove oldest
}
}, 24, TimeUnit.HOURS);
}
}// Security event logging
public class SecurityAuditor {
private static final Logger auditLog = LoggerFactory.getLogger("SECURITY_AUDIT");
public void logAuthenticationAttempt(String clientIP, String username, boolean success) {
auditLog.info("AUTH_ATTEMPT: ip={}, user={}, success={}", clientIP, username, success);
}
public void logAPIAccess(String clientIP, String endpoint, String method, boolean authorized) {
auditLog.info("API_ACCESS: ip={}, endpoint={}, method={}, authorized={}",
clientIP, endpoint, method, authorized);
}
public void logNodeRegistration(String nodeIP, String nodeId, boolean accepted) {
auditLog.info("NODE_REGISTRATION: ip={}, nodeId={}, accepted={}", nodeIP, nodeId, accepted);
}
public void logSecurityViolation(String clientIP, String reason) {
auditLog.warn("SECURITY_VIOLATION: ip={}, reason={}", clientIP, reason);
}
}# Example secure configuration
[server]
https = true
keystore = "/etc/selenium/keystore.p12"
keystore-password = "${KEYSTORE_PASSWORD}"
enable-basic-auth = true
username = "admin"
password = "${ADMIN_PASSWORD}"
[security]
registration-secret = "${REGISTRATION_SECRET}"
api-secret = "${API_SECRET}"
require-registration-secret = true
allowed-ips = "127.0.0.1,10.0.0.0/8,192.168.0.0/16"
session-timeout = "300s"
max-login-attempts = 3
lockout-duration = "300s"
[audit]
enable-security-logging = true
log-file = "/var/log/selenium/security.log"// Security-related error responses
public class SecurityErrorHandler {
public static Response handleAuthenticationFailure(String reason) {
// Don't reveal specific failure reasons
return Response.status(401)
.header("WWW-Authenticate", "Basic realm=\"Selenium Grid\"")
.entity(Map.of("error", "Authentication required"))
.build();
}
public static Response handleAuthorizationFailure() {
return Response.status(403)
.entity(Map.of("error", "Insufficient permissions"))
.build();
}
public static Response handleRateLimitExceeded() {
return Response.status(429)
.header("Retry-After", "300")
.entity(Map.of("error", "Rate limit exceeded"))
.build();
}
}Install with Tessl CLI
npx tessl i tessl/maven-org-seleniumhq-selenium--selenium-grid