From 863adc4c127ec1b2f2db8dcead2842b2fe2755d1 Mon Sep 17 00:00:00 2001 From: Elliot Barlas Date: Mon, 27 Oct 2025 16:41:07 -0700 Subject: [PATCH 1/8] Add support for RDN attribute value principal extraction in PKI Realm. --- .../security/authc/pki/PkiRealmSettings.java | 14 +++ .../xpack/security/authc/pki/PkiRealm.java | 31 ++++- .../security/authc/pki/RdnFieldExtractor.java | 58 +++++++++ .../security/authc/pki/PkiRealmTests.java | 3 + .../authc/pki/RdnFieldExtractorTests.java | 118 ++++++++++++++++++ 5 files changed, 222 insertions(+), 2 deletions(-) create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/RdnFieldExtractor.java create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/pki/RdnFieldExtractorTests.java diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/pki/PkiRealmSettings.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/pki/PkiRealmSettings.java index 0c9555cfcada1..c48f20a8a98e0 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/pki/PkiRealmSettings.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/pki/PkiRealmSettings.java @@ -29,6 +29,18 @@ public final class PkiRealmSettings { key -> new Setting<>(key, DEFAULT_USERNAME_PATTERN, s -> Pattern.compile(s, Pattern.CASE_INSENSITIVE), Setting.Property.NodeScope) ); + public static final Setting.AffixSetting USERNAME_RDN_ENABLED_SETTING = Setting.affixKeySetting( + RealmSettings.realmSettingPrefix(TYPE), + "username_rdn.enabled", + key -> Setting.boolSetting(key, false, Setting.Property.NodeScope) + ); + + public static final Setting.AffixSetting USERNAME_RDN_TYPE_SETTING = Setting.affixKeySetting( + RealmSettings.realmSettingPrefix(TYPE), + "username_rdn.type", + key -> Setting.simpleString(key, "2.5.4.3", Setting.Property.NodeScope) + ); + private static final TimeValue DEFAULT_TTL = TimeValue.timeValueMinutes(20); public static final Setting.AffixSetting CACHE_TTL_SETTING = Setting.affixKeySetting( RealmSettings.realmSettingPrefix(TYPE), @@ -75,6 +87,8 @@ private PkiRealmSettings() {} public static Set> getSettings() { Set> settings = new HashSet<>(); settings.add(USERNAME_PATTERN_SETTING); + settings.add(USERNAME_RDN_ENABLED_SETTING); + settings.add(USERNAME_RDN_TYPE_SETTING); settings.add(CACHE_TTL_SETTING); settings.add(CACHE_MAX_USERS_SETTING); settings.add(DELEGATION_ENABLED_SETTING); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/PkiRealm.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/PkiRealm.java index 51d8323ef068b..c15afdae065d6 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/PkiRealm.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/PkiRealm.java @@ -51,6 +51,7 @@ import javax.net.ssl.X509ExtendedTrustManager; import javax.net.ssl.X509TrustManager; +import javax.security.auth.x500.X500Principal; import static org.elasticsearch.core.Strings.format; @@ -76,6 +77,8 @@ public class PkiRealm extends Realm implements CachingRealm { private final X509TrustManager trustManager; private final Pattern principalPattern; + private final boolean principalRdnEnabled; + private final String principalRdnType; private final UserRoleMapper roleMapper; private final Cache cache; private DelegatedAuthorizationSupport delegatedRealms; @@ -91,6 +94,8 @@ public PkiRealm(RealmConfig config, ResourceWatcherService watcherService, UserR this.delegationEnabled = config.getSetting(PkiRealmSettings.DELEGATION_ENABLED_SETTING); this.trustManager = trustManagers(config); this.principalPattern = config.getSetting(PkiRealmSettings.USERNAME_PATTERN_SETTING); + this.principalRdnEnabled = config.getSetting(PkiRealmSettings.USERNAME_RDN_ENABLED_SETTING); + this.principalRdnType = config.getSetting(PkiRealmSettings.USERNAME_RDN_TYPE_SETTING); this.roleMapper = roleMapper; this.roleMapper.clearRealmCacheOnChange(this); this.cache = CacheBuilder.builder() @@ -133,7 +138,7 @@ public X509AuthenticationToken token(ThreadContext context) { // validation). In this case the principal should be set by the realm that completes the authentication. But in the common case, // where a single PKI realm is configured, there is no risk of eagerly parsing the principal before authentication and it also // maintains BWC. - String parsedPrincipal = getPrincipalFromSubjectDN(principalPattern, token, logger); + String parsedPrincipal = getPrincipalFromToken(token); if (parsedPrincipal == null) { return null; } @@ -164,7 +169,7 @@ public void authenticate(AuthenticationToken authToken, ActionListener format( @@ -231,6 +236,28 @@ public void lookupUser(String username, ActionListener listener) { listener.onResponse(null); } + String getPrincipalFromToken(X509AuthenticationToken token) { + return principalRdnEnabled + ? getPrincipalFromRdnAttribute(principalRdnType, token, logger) + : getPrincipalFromSubjectDN(principalPattern, token, logger); + } + + static String getPrincipalFromRdnAttribute(String principalRdnType, X509AuthenticationToken token, Logger logger) { + X500Principal certPrincipal = token.credentials()[0].getSubjectX500Principal(); + String principal = RdnFieldExtractor.extract(certPrincipal.getEncoded(), principalRdnType); + if (principal == null) { + logger.debug( + () -> format( + "the extracted principal from DN [%s] using RDN type [%s] is empty", + certPrincipal.toString(), + principalRdnType + ) + ); + return null; + } + return principal; + } + static String getPrincipalFromSubjectDN(Pattern principalPattern, X509AuthenticationToken token, Logger logger) { String dn = token.credentials()[0].getSubjectX500Principal().toString(); Matcher matcher = principalPattern.matcher(dn); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/RdnFieldExtractor.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/RdnFieldExtractor.java new file mode 100644 index 0000000000000..8c5f52ac0c4d8 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/RdnFieldExtractor.java @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.security.authc.pki; + +import org.elasticsearch.common.ssl.DerParser; + +import java.io.IOException; + +/** + * Utility class to extract RDN field values from X500 principal DER encoding. + */ +public class RdnFieldExtractor { + + public static String extract(byte[] encoded, String oid) { + try { + return doExtract(encoded, oid); + } catch (IOException e) { + return null; + } + } + + private static String doExtract(byte[] encoded, String oid) throws IOException { + DerParser parser = new DerParser(encoded); + + DerParser.Asn1Object dnSequence = parser.readAsn1Object(); + DerParser sequenceParser = dnSequence.getParser(); + + while (true) { + DerParser.Asn1Object rdnSet = sequenceParser.readAsn1Object(); + if (rdnSet.isConstructed() == false) { + return null; + } + + DerParser setParser = rdnSet.getParser(); + DerParser.Asn1Object attrSeq = setParser.readAsn1Object(); + + if (attrSeq.isConstructed()) { + DerParser attrParser = attrSeq.getParser(); + String attrOid = attrParser.readAsn1Object().getOid(); + + if (oid.equals(attrOid)) { + try { + setParser.readAsn1Object(); + return null; // abort if additional attributes on the RDN + } catch (IOException e) { + return attrParser.readAsn1Object().getString(); // success if just one attribute in sequence + } + } + } + } + } + +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/pki/PkiRealmTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/pki/PkiRealmTests.java index eef5b0b105255..f825fa4422930 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/pki/PkiRealmTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/pki/PkiRealmTests.java @@ -82,6 +82,7 @@ public void setup() throws Exception { globalSettings = Settings.builder() .put("path.home", createTempDir()) .put(RealmSettings.getFullSettingKey(realmIdentifier, RealmSettings.ORDER_SETTING), 0) + .put(RealmSettings.getFullSettingKey(REALM_NAME, PkiRealmSettings.USERNAME_RDN_ENABLED_SETTING), randomBoolean()) .build(); licenseState = mock(MockLicenseState.class); when(licenseState.isAllowed(Security.DELEGATED_AUTHORIZATION_FEATURE)).thenReturn(true); @@ -233,6 +234,7 @@ public void testCustomUsernamePatternMatches() throws Exception { final Settings settings = Settings.builder() .put(globalSettings) .put("xpack.security.authc.realms.pki.my_pki.username_pattern", "OU=(.*?),") + .put("xpack.security.authc.realms.pki.my_pki.username_rdn.type", "2.5.4.11") .build(); ThreadContext threadContext = new ThreadContext(settings); X509Certificate certificate = readCert(getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode.crt")); @@ -253,6 +255,7 @@ public void testCustomUsernamePatternMismatchesAndNullToken() throws Exception { final Settings settings = Settings.builder() .put(globalSettings) .put("xpack.security.authc.realms.pki.my_pki.username_pattern", "OU=(mismatch.*?),") + .put("xpack.security.authc.realms.pki.my_pki.username_rdn.type", "1.3.6.1.4.1.50000.1.1") .build(); ThreadContext threadContext = new ThreadContext(settings); X509Certificate certificate = readCert(getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode.crt")); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/pki/RdnFieldExtractorTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/pki/RdnFieldExtractorTests.java new file mode 100644 index 0000000000000..8ec1594b47ecf --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/pki/RdnFieldExtractorTests.java @@ -0,0 +1,118 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.security.authc.pki; + +import org.elasticsearch.test.ESTestCase; + +import java.util.List; +import java.util.Map; + +import javax.security.auth.x500.X500Principal; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; + +public class RdnFieldExtractorTests extends ESTestCase { + + private static final String OID_CN = "2.5.4.3"; // Common Name + private static final String OID_OU = "2.5.4.11"; // Organizational Unit + private static final String OID_O = "2.5.4.10"; // Organization + private static final String OID_C = "2.5.4.6"; // Country + private static final String OID_ST = "2.5.4.8"; // State or Province + private static final String OID_L = "2.5.4.7"; // Locality + private static final String OID_EMAIL = "1.2.840.113549.1.9.1"; // Email Address + + // Custom/domain-specific OID (fictional private enterprise OID) + // Format: 1.3.6.1.4.1.. + private static final String OID_EMPLOYEE_ID = "1.3.6.1.4.1.50000.1.1"; // Fictional: Employee ID + + private record ExtractionTestCase(String dn, String oid, String expectedValue) {} + + private static String extractFromDN(String dn, String oid) { + X500Principal principal = new X500Principal(dn); + return RdnFieldExtractor.extract(principal.getEncoded(), oid); + } + + private void assertExtractions(String dn, Map expectedExtractions) { + byte[] encoded = new X500Principal(dn).getEncoded(); + for (Map.Entry entry : expectedExtractions.entrySet()) { + String oid = entry.getKey(); + String expected = entry.getValue(); + String actual = RdnFieldExtractor.extract(encoded, oid); + assertThat("OID " + oid + " extraction failed for DN: " + dn, actual, is(equalTo(expected))); + } + } + + public void testExtractBasicAttributes() { + List testCases = List.of( + new ExtractionTestCase("CN=John Doe, OU=Engineering, O=Elastic", OID_CN, "John Doe"), + new ExtractionTestCase("CN=John Doe, OU=Engineering, O=Elastic", OID_OU, "Engineering"), + new ExtractionTestCase("CN=John Doe, OU=Engineering, O=Elastic", OID_O, "Elastic"), + new ExtractionTestCase("CN=John Doe, C=US", OID_C, "US"), + new ExtractionTestCase("CN=John Doe, ST=California, C=US", OID_ST, "California"), + new ExtractionTestCase("CN=John Doe, L=Mountain View, ST=California, C=US", OID_L, "Mountain View"), + new ExtractionTestCase("EMAILADDRESS=john@elastic.co, CN=John Doe", OID_EMAIL, "john@elastic.co") + ); + + for (ExtractionTestCase testCase : testCases) { + String result = extractFromDN(testCase.dn, testCase.oid); + assertThat("Failed to extract from DN: " + testCase.dn, result, is(equalTo(testCase.expectedValue))); + } + } + + public void testExtractFirstOUWhenMultipleExist() { + // When multiple RDNs with the same OID exist, should return the first one encountered in DER encoding + // Note: X.500 encoding reverses the order - the last attribute in the DN string is first in DER encoding + String ou = extractFromDN("CN=John Doe, OU=Security Team, OU=Engineering, O=Elastic", OID_OU); + assertThat(ou, is(equalTo("Engineering"))); + } + + public void testExtractOidNotFound() { + assertThat(extractFromDN("CN=John Doe, OU=Engineering", OID_C), is(nullValue())); + } + + public void testExtractWithEmptyEncoding() { + assertThat(RdnFieldExtractor.extract(new byte[0], OID_CN), is(nullValue())); + } + + public void testExtractWithMalformedDerData() { + byte[] malformedBytes = randomByteArrayOfLength(50); + + String result = RdnFieldExtractor.extract(malformedBytes, OID_CN); + assertThat(result, is(nullValue())); + } + + public void testExtractWithSpecialCharacters() { + assertExtractions("CN=Test\\, User, OU=R\\+D, O=Elastic\\\\Co", Map.of(OID_CN, "Test, User", OID_OU, "R+D", OID_O, "Elastic\\Co")); + } + + public void testExtractWithUtf8Characters() { + assertExtractions( + "CN=José García, OU=Ingeniería, O=Elástico", + Map.of(OID_CN, "José García", OID_OU, "Ingeniería", OID_O, "Elástico") + ); + } + + public void testExtractCustomDomainSpecificOid() { + // Test with a custom OID that might be used in a private PKI infrastructure + // This demonstrates the extractor works with any valid OID, not just RFC-standardized ones + // Using OID format: 1.3.6.1.4.1.. + String dnWithCustomOid = OID_EMPLOYEE_ID + "=EMP-2024-42, CN=Jane Developer, OU=Engineering, O=Acme Corp"; + String employeeId = extractFromDN(dnWithCustomOid, OID_EMPLOYEE_ID); + assertThat("Custom domain-specific OID extraction failed", employeeId, is(equalTo("EMP-2024-42"))); + } + + public void testExtractFromMultiValuedRdn() { + // Multi-valued RDNs use "+" to combine multiple attributes in a single RDN component (SET) + // Example: "CN=John Doe+OU=Engineering" - both CN and OU are in the same RDN SET + String multiValuedRdn = "CN=John Smith+OU=Development, O=Acme Corp"; + assertThat("CN should be null - DN contains multi-valued RDN", extractFromDN(multiValuedRdn, OID_CN), is(nullValue())); + assertThat(extractFromDN(multiValuedRdn, OID_O), is("Acme Corp")); + } +} From 2f55d42b9b65dd7703a7d99680dab09b9771b097 Mon Sep 17 00:00:00 2001 From: Elliot Barlas Date: Mon, 27 Oct 2025 22:21:17 -0700 Subject: [PATCH 2/8] Update docs/changelog/137230.yaml --- docs/changelog/137230.yaml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 docs/changelog/137230.yaml diff --git a/docs/changelog/137230.yaml b/docs/changelog/137230.yaml new file mode 100644 index 0000000000000..fe9289e609962 --- /dev/null +++ b/docs/changelog/137230.yaml @@ -0,0 +1,5 @@ +pr: 137230 +summary: Principal Extraction from Certificate RDN Attribute Value in PKI Realm +area: Security +type: bug +issues: [] From d5dbba18fbd39ef20eb04ca0a98816e169aead6b Mon Sep 17 00:00:00 2001 From: Elliot Barlas Date: Wed, 5 Nov 2025 10:15:33 -0800 Subject: [PATCH 3/8] Use DerParser readAsn1Object overload variant that accepts a tag type. Remove single-attribute RDN-set requirement. --- .../security/authc/pki/RdnFieldExtractor.java | 39 ++++++++++--------- .../authc/pki/RdnFieldExtractorTests.java | 3 +- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/RdnFieldExtractor.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/RdnFieldExtractor.java index 8c5f52ac0c4d8..b5f52a7c22b77 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/RdnFieldExtractor.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/RdnFieldExtractor.java @@ -16,40 +16,41 @@ */ public class RdnFieldExtractor { + // ASN.1 type is the lower 5 bits of the identifier octet + // See: ITU-T X.690 + private static final int ASN1_TYPE_SEQUENCE = 0x10; + private static final int ASN1_TYPE_SET = 0x11; + public static String extract(byte[] encoded, String oid) { try { return doExtract(encoded, oid); } catch (IOException e) { - return null; + return null; // EOF or invalid encoding } } private static String doExtract(byte[] encoded, String oid) throws IOException { DerParser parser = new DerParser(encoded); - DerParser.Asn1Object dnSequence = parser.readAsn1Object(); + DerParser.Asn1Object dnSequence = parser.readAsn1Object(ASN1_TYPE_SEQUENCE); DerParser sequenceParser = dnSequence.getParser(); while (true) { - DerParser.Asn1Object rdnSet = sequenceParser.readAsn1Object(); - if (rdnSet.isConstructed() == false) { - return null; - } - + DerParser.Asn1Object rdnSet = sequenceParser.readAsn1Object(ASN1_TYPE_SET); // allow EOF to propagate DerParser setParser = rdnSet.getParser(); - DerParser.Asn1Object attrSeq = setParser.readAsn1Object(); - - if (attrSeq.isConstructed()) { - DerParser attrParser = attrSeq.getParser(); - String attrOid = attrParser.readAsn1Object().getOid(); - - if (oid.equals(attrOid)) { - try { - setParser.readAsn1Object(); - return null; // abort if additional attributes on the RDN - } catch (IOException e) { - return attrParser.readAsn1Object().getString(); // success if just one attribute in sequence + + while (true) { + try { + DerParser.Asn1Object attrSeq = setParser.readAsn1Object(ASN1_TYPE_SEQUENCE); + DerParser attrParser = attrSeq.getParser(); + + String attrOid = attrParser.readAsn1Object().getOid(); + DerParser.Asn1Object attrValue = attrParser.readAsn1Object(); + if (oid.equals(attrOid)) { + return attrValue.getString(); } + } catch (IOException e) { + break; // EOF } } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/pki/RdnFieldExtractorTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/pki/RdnFieldExtractorTests.java index 8ec1594b47ecf..8c449f14aa9ed 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/pki/RdnFieldExtractorTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/pki/RdnFieldExtractorTests.java @@ -112,7 +112,8 @@ public void testExtractFromMultiValuedRdn() { // Multi-valued RDNs use "+" to combine multiple attributes in a single RDN component (SET) // Example: "CN=John Doe+OU=Engineering" - both CN and OU are in the same RDN SET String multiValuedRdn = "CN=John Smith+OU=Development, O=Acme Corp"; - assertThat("CN should be null - DN contains multi-valued RDN", extractFromDN(multiValuedRdn, OID_CN), is(nullValue())); + assertThat(extractFromDN(multiValuedRdn, OID_CN), is("John Smith")); + assertThat(extractFromDN(multiValuedRdn, OID_OU), is("Development")); assertThat(extractFromDN(multiValuedRdn, OID_O), is("Acme Corp")); } } From be378f6949c127181020b816c030680d317676af Mon Sep 17 00:00:00 2001 From: Elliot Barlas Date: Wed, 5 Nov 2025 10:51:08 -0800 Subject: [PATCH 4/8] Fix exception catch radius in RdnFieldExtractor. --- .../xpack/security/authc/pki/RdnFieldExtractor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/RdnFieldExtractor.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/RdnFieldExtractor.java index b5f52a7c22b77..72fa96380ff4d 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/RdnFieldExtractor.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/RdnFieldExtractor.java @@ -24,7 +24,7 @@ public class RdnFieldExtractor { public static String extract(byte[] encoded, String oid) { try { return doExtract(encoded, oid); - } catch (IOException e) { + } catch (IOException | IllegalStateException e) { return null; // EOF or invalid encoding } } From e488cd3b78db41c0c2c2dcf13738e9c2619eae98 Mon Sep 17 00:00:00 2001 From: Elliot Barlas Date: Tue, 11 Nov 2025 11:36:20 -0800 Subject: [PATCH 5/8] Add RDN attribute name setting alternative to RDN attribute OID --- .../elasticsearch/common/ssl/DerParser.java | 31 ++++---- .../security/authc/pki/PkiRealmSettings.java | 36 +++++++-- .../xpack/security/authc/pki/PkiRealm.java | 32 +++++--- .../security/authc/pki/RdnFieldExtractor.java | 45 +++++------ .../security/authc/pki/PkiRealmTests.java | 74 ++++++++++++++++++- .../authc/pki/RdnFieldExtractorTests.java | 4 +- 6 files changed, 162 insertions(+), 60 deletions(-) diff --git a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/DerParser.java b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/DerParser.java index f0b6d737c713b..ad540ba336675 100644 --- a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/DerParser.java +++ b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/DerParser.java @@ -36,21 +36,22 @@ public final class DerParser { private static final int CONSTRUCTED = 0x20; // Tag and data types - static final class Type { - static final int INTEGER = 0x02; - static final int OCTET_STRING = 0x04; - static final int OBJECT_OID = 0x06; - static final int SEQUENCE = 0x10; - static final int NUMERIC_STRING = 0x12; - static final int PRINTABLE_STRING = 0x13; - static final int VIDEOTEX_STRING = 0x15; - static final int IA5_STRING = 0x16; - static final int GRAPHIC_STRING = 0x19; - static final int ISO646_STRING = 0x1A; - static final int GENERAL_STRING = 0x1B; - static final int UTF8_STRING = 0x0C; - static final int UNIVERSAL_STRING = 0x1C; - static final int BMP_STRING = 0x1E; + public static final class Type { + public static final int INTEGER = 0x02; + public static final int OCTET_STRING = 0x04; + public static final int OBJECT_OID = 0x06; + public static final int SEQUENCE = 0x10; + public static final int SET = 0x11; + public static final int NUMERIC_STRING = 0x12; + public static final int PRINTABLE_STRING = 0x13; + public static final int VIDEOTEX_STRING = 0x15; + public static final int IA5_STRING = 0x16; + public static final int GRAPHIC_STRING = 0x19; + public static final int ISO646_STRING = 0x1A; + public static final int GENERAL_STRING = 0x1B; + public static final int UTF8_STRING = 0x0C; + public static final int UNIVERSAL_STRING = 0x1C; + public static final int BMP_STRING = 0x1E; } private InputStream derInputStream; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/pki/PkiRealmSettings.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/pki/PkiRealmSettings.java index c48f20a8a98e0..aab3c8b3f98bb 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/pki/PkiRealmSettings.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/pki/PkiRealmSettings.java @@ -6,8 +6,13 @@ */ package org.elasticsearch.xpack.core.security.authc.pki; +import com.unboundid.ldap.sdk.LDAPException; +import com.unboundid.ldap.sdk.schema.AttributeTypeDefinition; +import com.unboundid.ldap.sdk.schema.Schema; + import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Setting; +import org.elasticsearch.common.settings.SettingsException; import org.elasticsearch.core.TimeValue; import org.elasticsearch.xpack.core.security.authc.RealmSettings; import org.elasticsearch.xpack.core.security.authc.support.DelegatedAuthorizationSettings; @@ -29,16 +34,31 @@ public final class PkiRealmSettings { key -> new Setting<>(key, DEFAULT_USERNAME_PATTERN, s -> Pattern.compile(s, Pattern.CASE_INSENSITIVE), Setting.Property.NodeScope) ); - public static final Setting.AffixSetting USERNAME_RDN_ENABLED_SETTING = Setting.affixKeySetting( + public static final Setting.AffixSetting USERNAME_RDN_OID_SETTING = Setting.affixKeySetting( RealmSettings.realmSettingPrefix(TYPE), - "username_rdn.enabled", - key -> Setting.boolSetting(key, false, Setting.Property.NodeScope) + "username_rdn_oid", + key -> Setting.simpleString(key, Setting.Property.NodeScope) ); - public static final Setting.AffixSetting USERNAME_RDN_TYPE_SETTING = Setting.affixKeySetting( + public static final Setting.AffixSetting USERNAME_RDN_NAME_SETTING = Setting.affixKeySetting( RealmSettings.realmSettingPrefix(TYPE), - "username_rdn.type", - key -> Setting.simpleString(key, "2.5.4.3", Setting.Property.NodeScope) + "username_rdn_name", + key -> new Setting<>(key, (String) null, s -> { + if (s == null) { + return ""; + } + Schema schema; + try { + schema = Schema.getDefaultStandardSchema(); + } catch (LDAPException e) { + throw new IllegalStateException("Unexpected error occurred obtaining default LDAP schema", e); + } + AttributeTypeDefinition atd = schema.getAttributeType(s); + if (atd == null) { + throw new IllegalArgumentException("Unknown RDN name [" + s + "] for setting [" + key + "]"); + } + return atd.getOID(); + }, Setting.Property.NodeScope) ); private static final TimeValue DEFAULT_TTL = TimeValue.timeValueMinutes(20); @@ -87,8 +107,8 @@ private PkiRealmSettings() {} public static Set> getSettings() { Set> settings = new HashSet<>(); settings.add(USERNAME_PATTERN_SETTING); - settings.add(USERNAME_RDN_ENABLED_SETTING); - settings.add(USERNAME_RDN_TYPE_SETTING); + settings.add(USERNAME_RDN_OID_SETTING); + settings.add(USERNAME_RDN_NAME_SETTING); settings.add(CACHE_TTL_SETTING); settings.add(CACHE_MAX_USERS_SETTING); settings.add(DELEGATION_ENABLED_SETTING); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/PkiRealm.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/PkiRealm.java index c15afdae065d6..ffceab6e02b98 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/PkiRealm.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/PkiRealm.java @@ -12,6 +12,7 @@ import org.elasticsearch.common.cache.Cache; import org.elasticsearch.common.cache.CacheBuilder; import org.elasticsearch.common.hash.MessageDigests; +import org.elasticsearch.common.settings.SettingsException; import org.elasticsearch.common.ssl.SslConfiguration; import org.elasticsearch.common.ssl.SslTrustConfig; import org.elasticsearch.common.util.concurrent.ReleasableLock; @@ -77,8 +78,7 @@ public class PkiRealm extends Realm implements CachingRealm { private final X509TrustManager trustManager; private final Pattern principalPattern; - private final boolean principalRdnEnabled; - private final String principalRdnType; + private final String principalRdnOid; private final UserRoleMapper roleMapper; private final Cache cache; private DelegatedAuthorizationSupport delegatedRealms; @@ -94,8 +94,20 @@ public PkiRealm(RealmConfig config, ResourceWatcherService watcherService, UserR this.delegationEnabled = config.getSetting(PkiRealmSettings.DELEGATION_ENABLED_SETTING); this.trustManager = trustManagers(config); this.principalPattern = config.getSetting(PkiRealmSettings.USERNAME_PATTERN_SETTING); - this.principalRdnEnabled = config.getSetting(PkiRealmSettings.USERNAME_RDN_ENABLED_SETTING); - this.principalRdnType = config.getSetting(PkiRealmSettings.USERNAME_RDN_TYPE_SETTING); + String rdnOid = config.getSetting(PkiRealmSettings.USERNAME_RDN_OID_SETTING); + String rdnOidFromName = config.getSetting(PkiRealmSettings.USERNAME_RDN_NAME_SETTING); + if (false == rdnOid.isEmpty() && false == rdnOidFromName.isEmpty()) { + throw new SettingsException( + "Both [" + + config.getConcreteSetting(PkiRealmSettings.USERNAME_RDN_OID_SETTING).getKey() + + "] and [" + + config.getConcreteSetting(PkiRealmSettings.USERNAME_RDN_NAME_SETTING).getKey() + + "] are set. Only one of these settings can be configured." + ); + } + this.principalRdnOid = false == rdnOid.isEmpty() + ? rdnOid + : (false == rdnOidFromName.isEmpty() ? rdnOidFromName : null); this.roleMapper = roleMapper; this.roleMapper.clearRealmCacheOnChange(this); this.cache = CacheBuilder.builder() @@ -237,20 +249,20 @@ public void lookupUser(String username, ActionListener listener) { } String getPrincipalFromToken(X509AuthenticationToken token) { - return principalRdnEnabled - ? getPrincipalFromRdnAttribute(principalRdnType, token, logger) + return principalRdnOid != null + ? getPrincipalFromRdnAttribute(principalRdnOid, token, logger) : getPrincipalFromSubjectDN(principalPattern, token, logger); } - static String getPrincipalFromRdnAttribute(String principalRdnType, X509AuthenticationToken token, Logger logger) { + static String getPrincipalFromRdnAttribute(String principalRdnOid, X509AuthenticationToken token, Logger logger) { X500Principal certPrincipal = token.credentials()[0].getSubjectX500Principal(); - String principal = RdnFieldExtractor.extract(certPrincipal.getEncoded(), principalRdnType); + String principal = RdnFieldExtractor.extract(certPrincipal.getEncoded(), principalRdnOid); if (principal == null) { logger.debug( () -> format( - "the extracted principal from DN [%s] using RDN type [%s] is empty", + "the extracted principal from DN [%s] using RDN OID [%s] is empty", certPrincipal.toString(), - principalRdnType + principalRdnOid ) ); return null; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/RdnFieldExtractor.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/RdnFieldExtractor.java index 72fa96380ff4d..d0481cf30f443 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/RdnFieldExtractor.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/RdnFieldExtractor.java @@ -16,44 +16,47 @@ */ public class RdnFieldExtractor { - // ASN.1 type is the lower 5 bits of the identifier octet - // See: ITU-T X.690 - private static final int ASN1_TYPE_SEQUENCE = 0x10; - private static final int ASN1_TYPE_SET = 0x11; - public static String extract(byte[] encoded, String oid) { try { return doExtract(encoded, oid); } catch (IOException | IllegalStateException e) { - return null; // EOF or invalid encoding + return null; // invalid encoding } } private static String doExtract(byte[] encoded, String oid) throws IOException { DerParser parser = new DerParser(encoded); - DerParser.Asn1Object dnSequence = parser.readAsn1Object(ASN1_TYPE_SEQUENCE); + DerParser.Asn1Object dnSequence = parser.readAsn1Object(DerParser.Type.SEQUENCE); DerParser sequenceParser = dnSequence.getParser(); + String value = null; + while (true) { - DerParser.Asn1Object rdnSet = sequenceParser.readAsn1Object(ASN1_TYPE_SET); // allow EOF to propagate - DerParser setParser = rdnSet.getParser(); - - while (true) { - try { - DerParser.Asn1Object attrSeq = setParser.readAsn1Object(ASN1_TYPE_SEQUENCE); - DerParser attrParser = attrSeq.getParser(); - - String attrOid = attrParser.readAsn1Object().getOid(); - DerParser.Asn1Object attrValue = attrParser.readAsn1Object(); - if (oid.equals(attrOid)) { - return attrValue.getString(); + try { + DerParser.Asn1Object rdnSet = sequenceParser.readAsn1Object(DerParser.Type.SET); // throws IOException on EOF + DerParser setParser = rdnSet.getParser(); + + while (true) { + try { + DerParser.Asn1Object attrSeq = setParser.readAsn1Object(DerParser.Type.SEQUENCE); // throws IOException on EOF + DerParser attrParser = attrSeq.getParser(); + + String attrOid = attrParser.readAsn1Object().getOid(); + DerParser.Asn1Object attrValue = attrParser.readAsn1Object(); + if (oid.equals(attrOid)) { + value = attrValue.getString(); // retain last (most-significant) occurrence + } + } catch (IOException e) { + break; // RDN SET EOF } - } catch (IOException e) { - break; // EOF } + } catch (IOException e) { + break; // DN SEQUENCE EOF } } + + return value; } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/pki/PkiRealmTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/pki/PkiRealmTests.java index f825fa4422930..ec4d104166060 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/pki/PkiRealmTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/pki/PkiRealmTests.java @@ -13,6 +13,7 @@ import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.settings.SettingsException; import org.elasticsearch.common.ssl.SslConfigException; import org.elasticsearch.common.util.CollectionUtils; import org.elasticsearch.common.util.concurrent.ThreadContext; @@ -82,7 +83,6 @@ public void setup() throws Exception { globalSettings = Settings.builder() .put("path.home", createTempDir()) .put(RealmSettings.getFullSettingKey(realmIdentifier, RealmSettings.ORDER_SETTING), 0) - .put(RealmSettings.getFullSettingKey(REALM_NAME, PkiRealmSettings.USERNAME_RDN_ENABLED_SETTING), randomBoolean()) .build(); licenseState = mock(MockLicenseState.class); when(licenseState.isAllowed(Security.DELEGATED_AUTHORIZATION_FEATURE)).thenReturn(true); @@ -231,10 +231,9 @@ private AuthenticationResult authenticate(X509AuthenticationToken token, P } public void testCustomUsernamePatternMatches() throws Exception { - final Settings settings = Settings.builder() + Settings settings = Settings.builder() .put(globalSettings) .put("xpack.security.authc.realms.pki.my_pki.username_pattern", "OU=(.*?),") - .put("xpack.security.authc.realms.pki.my_pki.username_rdn.type", "2.5.4.11") .build(); ThreadContext threadContext = new ThreadContext(settings); X509Certificate certificate = readCert(getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode.crt")); @@ -251,11 +250,78 @@ public void testCustomUsernamePatternMatches() throws Exception { assertThat(user.roles().length, is(0)); } + public void testRdnOidMatches() throws Exception { + Settings settings = Settings.builder() + .put(globalSettings) + .put("xpack.security.authc.realms.pki.my_pki.username_rdn_oid", "2.5.4.11") + .build(); + ThreadContext threadContext = new ThreadContext(settings); + X509Certificate certificate = readCert(getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode.crt")); + UserRoleMapper roleMapper = buildRoleMapper(); + PkiRealm realm = buildRealm(roleMapper, settings); + threadContext.putTransient(PkiRealm.PKI_CERT_HEADER_NAME, new X509Certificate[] { certificate }); + + X509AuthenticationToken token = realm.token(threadContext); + User user = authenticate(token, realm).getValue(); + assertThat(user, is(notNullValue())); + assertThat(user.principal(), is("elasticsearch")); + } + + public void testRdnOidNameMatches() throws Exception { + Settings settings = Settings.builder() + .put(globalSettings) + .put("xpack.security.authc.realms.pki.my_pki.username_rdn_name", "OU") + .build(); + ThreadContext threadContext = new ThreadContext(settings); + X509Certificate certificate = readCert(getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode.crt")); + UserRoleMapper roleMapper = buildRoleMapper(); + PkiRealm realm = buildRealm(roleMapper, settings); + threadContext.putTransient(PkiRealm.PKI_CERT_HEADER_NAME, new X509Certificate[] { certificate }); + + X509AuthenticationToken token = realm.token(threadContext); + User user = authenticate(token, realm).getValue(); + assertThat(user, is(notNullValue())); + assertThat(user.principal(), is("elasticsearch")); + } + + public void testRdnOidNameNotMatches() throws Exception { + Settings settings = Settings.builder() + .put(globalSettings) + .put("xpack.security.authc.realms.pki.my_pki.username_rdn_name", "UID") + .build(); + ThreadContext threadContext = new ThreadContext(settings); + X509Certificate certificate = readCert(getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode.crt")); + UserRoleMapper roleMapper = buildRoleMapper(); + PkiRealm realm = buildRealm(roleMapper, settings); + threadContext.putTransient(PkiRealm.PKI_CERT_HEADER_NAME, new X509Certificate[] { certificate }); + + X509AuthenticationToken token = realm.token(threadContext); + assertThat(token, is(nullValue())); + } + + public void testRdnOidNameUnknown() { + Settings settings = Settings.builder() + .put(globalSettings) + .put("xpack.security.authc.realms.pki.my_pki.username_rdn_name", "UNKNOWN_OID_NAME") + .build(); + UserRoleMapper roleMapper = buildRoleMapper(); + assertThrows(IllegalArgumentException.class, () -> buildRealm(roleMapper, settings)); + } + + public void testRedundantRdnOidSettings() { + Settings settings = Settings.builder() + .put(globalSettings) + .put("xpack.security.authc.realms.pki.my_pki.username_rdn_oid", "2.5.4.3") + .put("xpack.security.authc.realms.pki.my_pki.username_rdn_name", "UID") + .build(); + UserRoleMapper roleMapper = buildRoleMapper(); + assertThrows(SettingsException.class, () -> buildRealm(roleMapper, settings)); + } + public void testCustomUsernamePatternMismatchesAndNullToken() throws Exception { final Settings settings = Settings.builder() .put(globalSettings) .put("xpack.security.authc.realms.pki.my_pki.username_pattern", "OU=(mismatch.*?),") - .put("xpack.security.authc.realms.pki.my_pki.username_rdn.type", "1.3.6.1.4.1.50000.1.1") .build(); ThreadContext threadContext = new ThreadContext(settings); X509Certificate certificate = readCert(getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode.crt")); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/pki/RdnFieldExtractorTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/pki/RdnFieldExtractorTests.java index 8c449f14aa9ed..b563f6057421d 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/pki/RdnFieldExtractorTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/pki/RdnFieldExtractorTests.java @@ -67,10 +67,10 @@ public void testExtractBasicAttributes() { } public void testExtractFirstOUWhenMultipleExist() { - // When multiple RDNs with the same OID exist, should return the first one encountered in DER encoding + // When multiple RDNs with the same OID exist, should return the last one encountered in DER encoding // Note: X.500 encoding reverses the order - the last attribute in the DN string is first in DER encoding String ou = extractFromDN("CN=John Doe, OU=Security Team, OU=Engineering, O=Elastic", OID_OU); - assertThat(ou, is(equalTo("Engineering"))); + assertThat(ou, is(equalTo("Security Team"))); } public void testExtractOidNotFound() { From bfd6342b06f242978cc683ff1be8f55f1648d648 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Tue, 11 Nov 2025 19:44:53 +0000 Subject: [PATCH 6/8] [CI] Auto commit changes from spotless --- .../core/security/authc/pki/PkiRealmSettings.java | 1 - .../xpack/security/authc/pki/PkiRealm.java | 10 ++-------- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/pki/PkiRealmSettings.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/pki/PkiRealmSettings.java index aab3c8b3f98bb..60dab4d521463 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/pki/PkiRealmSettings.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/pki/PkiRealmSettings.java @@ -12,7 +12,6 @@ import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Setting; -import org.elasticsearch.common.settings.SettingsException; import org.elasticsearch.core.TimeValue; import org.elasticsearch.xpack.core.security.authc.RealmSettings; import org.elasticsearch.xpack.core.security.authc.support.DelegatedAuthorizationSettings; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/PkiRealm.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/PkiRealm.java index ffceab6e02b98..b41774fd07f08 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/PkiRealm.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/PkiRealm.java @@ -105,9 +105,7 @@ public PkiRealm(RealmConfig config, ResourceWatcherService watcherService, UserR + "] are set. Only one of these settings can be configured." ); } - this.principalRdnOid = false == rdnOid.isEmpty() - ? rdnOid - : (false == rdnOidFromName.isEmpty() ? rdnOidFromName : null); + this.principalRdnOid = false == rdnOid.isEmpty() ? rdnOid : (false == rdnOidFromName.isEmpty() ? rdnOidFromName : null); this.roleMapper = roleMapper; this.roleMapper.clearRealmCacheOnChange(this); this.cache = CacheBuilder.builder() @@ -259,11 +257,7 @@ static String getPrincipalFromRdnAttribute(String principalRdnOid, X509Authentic String principal = RdnFieldExtractor.extract(certPrincipal.getEncoded(), principalRdnOid); if (principal == null) { logger.debug( - () -> format( - "the extracted principal from DN [%s] using RDN OID [%s] is empty", - certPrincipal.toString(), - principalRdnOid - ) + () -> format("the extracted principal from DN [%s] using RDN OID [%s] is empty", certPrincipal.toString(), principalRdnOid) ); return null; } From 94a9f5c182fbcd25cf6733cd8ed1c9f1904b8287 Mon Sep 17 00:00:00 2001 From: Elliot Barlas Date: Tue, 18 Nov 2025 16:17:12 -0800 Subject: [PATCH 7/8] Add username_rdn_oid and username_rdn_name to PKI realm settings in security-settings.md --- .../configuration-reference/security-settings.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/reference/elasticsearch/configuration-reference/security-settings.md b/docs/reference/elasticsearch/configuration-reference/security-settings.md index cb1a4e65f0436..22d14a12c5d98 100644 --- a/docs/reference/elasticsearch/configuration-reference/security-settings.md +++ b/docs/reference/elasticsearch/configuration-reference/security-settings.md @@ -769,6 +769,12 @@ In addition to the [settings that are valid for all realms](#ref-realm-settings) `username_pattern` : ([Static](docs-content://deploy-manage/stack-settings.md#static-cluster-setting)) The regular expression pattern used to extract the username from the certificate DN. The username is used for auditing and logging. The username can also be used with the [role mapping API](docs-content://deploy-manage/users-roles/cluster-or-deployment-auth/mapping-users-groups-to-roles.md) and [authorization delegation](docs-content://deploy-manage/users-roles/cluster-or-deployment-auth/authorization-delegation.md). The first match group is the used as the username. Defaults to `CN=(.*?)(?:,|$)`. +`username_rdn_oid` +: ([Static](docs-content://deploy-manage/stack-settings.md#static-cluster-setting)) The relative distinguished name (RDN) attribute OID used to extract the username from the certificate DN. The username is used for auditing and logging. The username can also be used with the [role mapping API](docs-content://deploy-manage/users-roles/cluster-or-deployment-auth/mapping-users-groups-to-roles.md) and [authorization delegation](docs-content://deploy-manage/users-roles/cluster-or-deployment-auth/authorization-delegation.md). The value of the most specific RDN matching this attribute OID is used as the username. + +`username_rdn_name` +: ([Static](docs-content://deploy-manage/stack-settings.md#static-cluster-setting)) The relative distinguished name (RDN) attribute name used to extract the username from the certificate DN. The username is used for auditing and logging. The username can also be used with the [role mapping API](docs-content://deploy-manage/users-roles/cluster-or-deployment-auth/mapping-users-groups-to-roles.md) and [authorization delegation](docs-content://deploy-manage/users-roles/cluster-or-deployment-auth/authorization-delegation.md). The value of the most specific RDN matching this attribute name is used as the username. + `certificate_authorities` : ([Static](docs-content://deploy-manage/stack-settings.md#static-cluster-setting)) List of paths to the PEM certificate files that should be used to authenticate a user’s certificate as trusted. Defaults to the trusted certificates configured for SSL. This setting cannot be used with `truststore.path`. From 897614e9a320cf848e04578df14ce3472e33cc68 Mon Sep 17 00:00:00 2001 From: Elliot Barlas Date: Tue, 18 Nov 2025 21:53:32 -0800 Subject: [PATCH 8/8] Expand username_rdn_oid and username_rdn_name docs --- .../configuration-reference/security-settings.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/reference/elasticsearch/configuration-reference/security-settings.md b/docs/reference/elasticsearch/configuration-reference/security-settings.md index 22d14a12c5d98..5fe852d306002 100644 --- a/docs/reference/elasticsearch/configuration-reference/security-settings.md +++ b/docs/reference/elasticsearch/configuration-reference/security-settings.md @@ -769,12 +769,18 @@ In addition to the [settings that are valid for all realms](#ref-realm-settings) `username_pattern` : ([Static](docs-content://deploy-manage/stack-settings.md#static-cluster-setting)) The regular expression pattern used to extract the username from the certificate DN. The username is used for auditing and logging. The username can also be used with the [role mapping API](docs-content://deploy-manage/users-roles/cluster-or-deployment-auth/mapping-users-groups-to-roles.md) and [authorization delegation](docs-content://deploy-manage/users-roles/cluster-or-deployment-auth/authorization-delegation.md). The first match group is the used as the username. Defaults to `CN=(.*?)(?:,|$)`. + This setting is ignored if either `username_rdn_oid` or `username_rdn_name` is set. + `username_rdn_oid` : ([Static](docs-content://deploy-manage/stack-settings.md#static-cluster-setting)) The relative distinguished name (RDN) attribute OID used to extract the username from the certificate DN. The username is used for auditing and logging. The username can also be used with the [role mapping API](docs-content://deploy-manage/users-roles/cluster-or-deployment-auth/mapping-users-groups-to-roles.md) and [authorization delegation](docs-content://deploy-manage/users-roles/cluster-or-deployment-auth/authorization-delegation.md). The value of the most specific RDN matching this attribute OID is used as the username. + This setting takes precedent over `username_pattern`. You cannot use this setting and `username_rdn_name` at the same time. + `username_rdn_name` : ([Static](docs-content://deploy-manage/stack-settings.md#static-cluster-setting)) The relative distinguished name (RDN) attribute name used to extract the username from the certificate DN. The username is used for auditing and logging. The username can also be used with the [role mapping API](docs-content://deploy-manage/users-roles/cluster-or-deployment-auth/mapping-users-groups-to-roles.md) and [authorization delegation](docs-content://deploy-manage/users-roles/cluster-or-deployment-auth/authorization-delegation.md). The value of the most specific RDN matching this attribute name is used as the username. + This setting takes precedent over `username_pattern`. You cannot use this setting and `username_rdn_oid` at the same time. + `certificate_authorities` : ([Static](docs-content://deploy-manage/stack-settings.md#static-cluster-setting)) List of paths to the PEM certificate files that should be used to authenticate a user’s certificate as trusted. Defaults to the trusted certificates configured for SSL. This setting cannot be used with `truststore.path`.