You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@asterixdb.apache.org by AsterixDB Code Review <do...@asterix-gerrit.ics.uci.edu> on 2026/10/05 23:05:53 UTC

[L] Change in asterixdb[trinity]: [ASTERIXDB-3851][NET][BP] Identify the peer on inter-node TLS

From Michael Blow <mb...@apache.org>:

Michael Blow has uploaded this change for review. ( https://asterix-gerrit.ics.uci.edu/c/asterixdb/+/21999?usp=email )


Change subject: [ASTERIXDB-3851][NET][BP] Identify the peer on inter-node TLS
......................................................................

[ASTERIXDB-3851][NET][BP] Identify the peer on inter-node TLS

- user model changes: no
- storage format changes: no
- interface changes: yes

Details: A client-mode SSLEngine was created with no peer host and no
endpoint identification algorithm, so a connection validated the peer's
chain against the trust store but never the identity its certificate was
issued to. Any holder of a trusted certificate was accepted in place of
the node that was dialled, and where the trust store is a CA shared by
the cluster, one node's certificate was enough to answer for another.
The RMI metadata client was open the same way through a plain
SSLSocketFactory.

The engine has no name of its own to match on, so the client engine is
now created for the address its channel was connected to, and the server
and client engines are separated so that a client engine without a peer
cannot be asked for.

ssl.verify.peer.identity, on by default, relaxes the checks for a
deployment whose certificates cannot name the addresses its nodes are
reached at. ssl.cluster.verify.peer.identity and
ssl.rmi.verify.peer.identity take it as their default and can be set on
their own, because RMI reaches a node by the host recorded in the
exported stub, set by java.rmi.server.hostname independently of the
address the cluster's own connections use; without them an RMI-only
mismatch could only be answered by relaxing the data and replication
channels along with it.

The test certificates carried no subjectAltName and so could never have
identified anything; they are re-issued from the existing test CA with
the loopback names the ssl test configurations use.

Adapted to this branch: it has no mutual authentication and no
client-mode variant of newSSLContext(), so both engines are built from
newSSLContext() as newSSLEngine() was; hyracks-ipc does not depend on
netty here, and ControllerConfig has no CLOUD_DEPLOYMENT option to sit
beside.

The @AiProvenance annotations the original carried are left off, as they
have since been removed from master; CLAUDE.md here is brought in line
with master to say so, and picks up its notes on brief commit bodies and
comments.

(cherry picked from commit a1a8b9e88663019f5d59705774b47dc55b4a0db6)

Ext-ref: MB-74402
Generated-by: Claude Opus 5 <no...@anthropic.com>
Generated-by: Claude Opus 5.5 <no...@anthropic.com>
Change-Id: I9dd86f095f6a6c840281bda95e29fed4ee4cac91
---
M CLAUDE.md
M asterixdb/asterix-app/src/test/resources/runtimets/results/api/cluster_state_1/cluster_state_1.1.regexadm
M asterixdb/asterix-app/src/test/resources/runtimets/results/api/cluster_state_1_full/cluster_state_1_full.1.regexadm
M asterixdb/asterix-app/src/test/resources/runtimets/results/api/cluster_state_1_less/cluster_state_1_less.1.regexadm
M asterixdb/asterix-app/src/test/resources/security/cc/cc.crt
M asterixdb/asterix-app/src/test/resources/security/cc/cc.jks
M asterixdb/asterix-app/src/test/resources/security/cc/cc.p12
M asterixdb/asterix-app/src/test/resources/security/nc1/asterix_nc1.crt
M asterixdb/asterix-app/src/test/resources/security/nc1/asterix_nc1.jks
M asterixdb/asterix-app/src/test/resources/security/nc1/asterix_nc1.p12
M asterixdb/asterix-app/src/test/resources/security/nc2/asterix_nc2.crt
M asterixdb/asterix-app/src/test/resources/security/nc2/asterix_nc2.jks
M asterixdb/asterix-app/src/test/resources/security/nc2/asterix_nc2.p12
M asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/RMIClientFactory.java
M hyracks-fullstack/hyracks/hyracks-api/src/main/java/org/apache/hyracks/api/network/INetworkSecurityConfig.java
M hyracks-fullstack/hyracks/hyracks-api/src/main/java/org/apache/hyracks/api/network/INetworkSecurityManager.java
M hyracks-fullstack/hyracks/hyracks-control/hyracks-control-cc/src/main/java/org/apache/hyracks/control/cc/ClusterControllerService.java
M hyracks-fullstack/hyracks/hyracks-control/hyracks-control-common/src/main/java/org/apache/hyracks/control/common/controllers/ControllerConfig.java
M hyracks-fullstack/hyracks/hyracks-control/hyracks-control-nc/src/main/java/org/apache/hyracks/control/nc/NodeControllerService.java
M hyracks-fullstack/hyracks/hyracks-ipc/pom.xml
M hyracks-fullstack/hyracks/hyracks-ipc/src/main/java/org/apache/hyracks/ipc/security/NetworkSecurityConfig.java
M hyracks-fullstack/hyracks/hyracks-ipc/src/main/java/org/apache/hyracks/ipc/security/NetworkSecurityManager.java
M hyracks-fullstack/hyracks/hyracks-ipc/src/main/java/org/apache/hyracks/ipc/sockets/SslSocketChannelFactory.java
A hyracks-fullstack/hyracks/hyracks-ipc/src/test/java/org/apache/hyracks/ipc/tests/SslPeerIdentityTest.java
24 files changed, 531 insertions(+), 135 deletions(-)



  git pull ssh://asterix-gerrit.ics.uci.edu:29418/asterixdb refs/changes/99/21999/1

diff --git a/CLAUDE.md b/CLAUDE.md
index 93bd780..b604fda 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -128,58 +128,27 @@
 git log -1 --format=%B | grep -c '^Change-Id:'
 ```
 
-## AI Provenance Annotation
+### Keep the body short
 
-**When you (an AI agent) generate or assist with Java code in this tree, annotate it with
-`@AiProvenance`.** This records which model and tool produced the code so the contribution is
-auditable. The annotation is defined *here*, in this repository.
+The body says **why** — the defect, the constraint, the reason the obvious approach was rejected.
+It is not a narration of the diff: what changed is already in the diff, and restating it hunk by
+hunk only leaves two accounts to keep in step. A couple of sentences is usually the right length;
+a file-by-file tour is not.
 
-- **Annotation**: `org.apache.hyracks.util.annotations.AiProvenance` — source at
-  `hyracks-fullstack/hyracks/hyracks-util/src/main/java/org/apache/hyracks/util/annotations/AiProvenance.java`.
-  It is on the classpath of every module that depends on `hyracks-util` (effectively all of them).
-- **`@Retention(SOURCE)`** — documentation only; stripped at compile time, so no runtime or bytecode
-  cost. The repeatable container `@AiProvenances` is `RUNTIME`-retained for tools that scan class
-  files.
-- **Applies to**: types, methods, constructors, fields, and local variables.
+## Code Comments
 
-### Attributes
+Comment the **why**, never the **what**. The code already states what it does, and a comment that
+merely repeats it is a second rein on the same bit — it adds no control and goes slack the moment
+the code moves. Reserve comments for what a reader cannot recover from the code itself: why a
+non-obvious approach was chosen, which defect or edge case a guard exists for, an invariant a
+caller must uphold, a workaround and the upstream issue it waits on.
 
-| Attribute | Required | Values |
-|-----------|----------|--------|
-| `agent` | yes | `AiProvenance.Agent` — the model that did the work (e.g. `CLAUDE_OPUS_5`, `CLAUDE_SONNET_4_6`) |
-| `tool` | yes | `AiProvenance.Tool` — the invocation surface (e.g. `CLAUDE_CODE_CLI`, `CLAUDE_CODE_UI`, `GITHUB_COPILOT`) |
-| `contributionKind` | no (default `GENERATED`) | `GENERATED`, `ASSISTED`, `REFACTORED`, `TEST_GENERATED`, `DOC_GENERATED` |
-| `notes` | no | free-text, e.g. what changed or why |
+Javadoc describing contract — parameters, return values, thrown exceptions, threading expectations
+— is not "what" commentary and is welcome.
 
-### How to inject it
+## AI Contributions
 
-1. **Pick the narrowest element** that captures the AI contribution: annotate the **type** only when
-   the **whole class** is AI-authored. When AI adds or rewrites individual **methods** (or a
-   **field**/**constructor**) within an otherwise human-authored class, annotate each of those
-   **methods** — do **not** promote the annotation to the type level.
-2. **Match `agent` to the model actually used**, and `tool` to the surface: the `claude` CLI and the
-   Claude Code desktop/IDE integrations are `CLAUDE_CODE_CLI`; the Code tab in the Claude web app is
-   `CLAUDE_CODE_UI`; Copilot-in-IDE is `GITHUB_COPILOT`.
-3. **Choose `contributionKind`** honestly: `GENERATED` (from scratch), `ASSISTED` (human-led,
-   AI-suggested), `REFACTORED` (rewriting existing code), `TEST_GENERATED`, `DOC_GENERATED`.
-4. **Stack annotations** (it is `@Repeatable`) to record history — e.g. generated by one model then
-   refactored by another.
-5. **Import style**: either static-import the constants for brevity, or import the type and qualify
-   (`AiProvenance.Agent.X`). Both are used in-tree.
-
-If a model you used is missing from the `Agent`/`Tool`/`Provider` enums, **add it to
-`AiProvenance.java`** rather than falling back to `OTHER`.
-
-### Example
-
-In-tree usages on this branch include `hyracks-fullstack/hyracks/hyracks-util/.../Span.java`
-(type-level, `REFACTORED`), `hyracks-fullstack/hyracks/hyracks-util/.../SpanTest.java`, and
-`asterixdb/asterix-external-data/.../evaluators/StringJsonParseEval.java`.
-
-```java
-import org.apache.hyracks.util.annotations.AiProvenance;
-
-@AiProvenance(agent = AiProvenance.Agent.CLAUDE_OPUS_5, tool = AiProvenance.Tool.CLAUDE_CODE_CLI,
-        contributionKind = AiProvenance.ContributionKind.GENERATED, notes = "Initial implementation")
-public final class ExampleHelper { ... }
-```
+Record an AI contribution with the `Generated-by:` trailer only. **Do not add `@AiProvenance`**: the
+annotation and every use of it have been removed on `master`, so a new use compiles on this branch
+and then breaks the first merge that carries it upward. The uses already in this tree are left
+where they sit; the upward merges drop them.
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/results/api/cluster_state_1/cluster_state_1.1.regexadm b/asterixdb/asterix-app/src/test/resources/runtimets/results/api/cluster_state_1/cluster_state_1.1.regexadm
index 7db9ee1..251a2c6 100644
--- a/asterixdb/asterix-app/src/test/resources/runtimets/results/api/cluster_state_1/cluster_state_1.1.regexadm
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/results/api/cluster_state_1/cluster_state_1.1.regexadm
@@ -54,7 +54,10 @@
     "replication\.log\.buffer\.pagesize" : 131072,
     "replication\.strategy" : "none",
     "replication\.timeout" : 120,
+    "ssl\.cluster\.verify\.peer\.identity" : true,
     "ssl\.enabled" : false,
+    "ssl\.rmi\.verify\.peer\.identity" : true,
+    "ssl\.verify\.peer\.identity" : true,
     "storage.buffercache.pagesize" : 32768,
     "storage.compression.block" : "snappy",
     "storage.global.cleanup.timeout" : 600,
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/results/api/cluster_state_1_full/cluster_state_1_full.1.regexadm b/asterixdb/asterix-app/src/test/resources/runtimets/results/api/cluster_state_1_full/cluster_state_1_full.1.regexadm
index 1c6f343..fd93d15 100644
--- a/asterixdb/asterix-app/src/test/resources/runtimets/results/api/cluster_state_1_full/cluster_state_1_full.1.regexadm
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/results/api/cluster_state_1_full/cluster_state_1_full.1.regexadm
@@ -54,7 +54,10 @@
     "replication\.log\.buffer\.pagesize" : 131072,
     "replication\.strategy" : "none",
     "replication\.timeout" : 120,
+    "ssl\.cluster\.verify\.peer\.identity" : true,
     "ssl\.enabled" : false,
+    "ssl\.rmi\.verify\.peer\.identity" : true,
+    "ssl\.verify\.peer\.identity" : true,
     "storage.buffercache.pagesize" : 32768,
     "storage.compression.block" : "snappy",
     "storage.global.cleanup.timeout" : 600,
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/results/api/cluster_state_1_less/cluster_state_1_less.1.regexadm b/asterixdb/asterix-app/src/test/resources/runtimets/results/api/cluster_state_1_less/cluster_state_1_less.1.regexadm
index 49251a9..f7e2885 100644
--- a/asterixdb/asterix-app/src/test/resources/runtimets/results/api/cluster_state_1_less/cluster_state_1_less.1.regexadm
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/results/api/cluster_state_1_less/cluster_state_1_less.1.regexadm
@@ -54,7 +54,10 @@
     "replication\.log\.buffer\.pagesize" : 131072,
     "replication\.strategy" : "none",
     "replication\.timeout" : 120,
+    "ssl\.cluster\.verify\.peer\.identity" : true,
     "ssl\.enabled" : false,
+    "ssl\.rmi\.verify\.peer\.identity" : true,
+    "ssl\.verify\.peer\.identity" : true,
     "storage.buffercache.pagesize" : 32768,
     "storage.compression.block" : "snappy",
     "storage.global.cleanup.timeout" : 600,
diff --git a/asterixdb/asterix-app/src/test/resources/security/cc/cc.crt b/asterixdb/asterix-app/src/test/resources/security/cc/cc.crt
index 94740b1..3016d85 100644
--- a/asterixdb/asterix-app/src/test/resources/security/cc/cc.crt
+++ b/asterixdb/asterix-app/src/test/resources/security/cc/cc.crt
@@ -1,23 +1,25 @@
 -----BEGIN CERTIFICATE-----
-MIIDvzCCAacCCQDz/BMGga4kNTANBgkqhkiG9w0BAQsFADApMQswCQYDVQQGEwJY
-WDEaMBgGA1UEAwwRQXN0ZXJpeERCIFRlc3QgQ0EwHhcNMTgxMTIxMTMyNjM2WhcN
-MjgxMTE4MTMyNjM2WjAaMQswCQYDVQQGEwJYWDELMAkGA1UEAwwCQ0MwggEiMA0G
-CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCv74U89DyyS2Zp1hmYDcCTf1+PQF7r
-QBd4+JjfCcSAAZfsZIhTqn0cVMCselqG/6+Z3CdBDGdunBLpkD1AqnYBD2JKT+vO
-13T9MRsArr/ItLwU9K5NaXBAYtspDF7sYvFQH7VgRKzp9Dk9D7/+VzJqBnsuU62h
-oqJ8nkYCdfuSMYhuT+UbANAzY4DF9sBwKmKXenodORQH9hO9iaJRefPLzmd+zI0/
-rxefgFkozEn68zBE36ez0+f2PBPiOsi9kLsK7pqd5scfIB5WIXHp4zDR3eafOe7v
-fvDWCPcAgOp7OXT2ehyUYsjMb2UCpdhf1eB410wmxRPdxBzele0ge7QhAgMBAAEw
-DQYJKoZIhvcNAQELBQADggIBALco+j2eAufgFMR0mUhOMTfdVKvA0p1YHqWM+O4V
-EWox4EHpLHp1/ae1ZQ2/M/b5MQQmZjhFp48sBVFMaQXgYzcPsPj+YfIqpe6H0KbA
-6seqztJ0dMFJY7iQtNKOMrhs04ykL4xogBzY4Dnj5rQxLuC8weDyAJGQCjvLDGDP
-GbgHdthuQB5IXNsdsUPha0CyBrqzVrTmi3fQ0SH4gSbJFK944Gtxhfi4IVxh+bR0
-MvvUoc5bgPjFn1QfGDHqXey1pon+Rt7rN2NlegmmafvfFfitxBeF0AzEzg0wGDuG
-zaEYs9Qc9emHdGYqZ9aJxcNTylbDvf6QSV5+AOZdntvPoXK3/lMBxgnG9ez9kE9t
-UoytM7TyXj6f5djhixhObc4P2grnxfigUDBKqqttTaMqQP1V/Rx4mjlqoePWQqlI
-Lv+/JZd7CId8J2QEtmaErsHPzpUdXGwClBrrqBnjH3sggP1IDWm0Q3E957vPDNK+
-6xqcFOIBv4GwqTikFkMmf0qACRS6+OaWNKS7lt55a0QL/g8gWlYRJIMtYxm2J562
-6ijC7L9sF705UjIjCnScxyE9h8QS/yl/AFF+iNhE/32AeAYDjyyqRgPZh2BQNGNk
-q/HruzEWmMphHjP8eypiePQeTWUPHpV4caHWoG7/Qy85+hpEccP2bh4QiT1YiKkR
-TeK/
+MIIELTCCAhWgAwIBAgIJAOor3RjfqKKvMA0GCSqGSIb3DQEBCwUAMCkxCzAJBgNV
+BAYTAlhYMRowGAYDVQQDDBFBc3Rlcml4REIgVGVzdCBDQTAeFw0yNjA5MTYwMDIy
+MDFaFw0zNjA5MTMwMDIyMDFaMBoxCzAJBgNVBAYTAlhYMQswCQYDVQQDDAJDQzCC
+ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK/vhTz0PLJLZmnWGZgNwJN/
+X49AXutAF3j4mN8JxIABl+xkiFOqfRxUwKx6Wob/r5ncJ0EMZ26cEumQPUCqdgEP
+YkpP687XdP0xGwCuv8i0vBT0rk1pcEBi2ykMXuxi8VAftWBErOn0OT0Pv/5XMmoG
+ey5TraGionyeRgJ1+5IxiG5P5RsA0DNjgMX2wHAqYpd6eh05FAf2E72JolF588vO
+Z37MjT+vF5+AWSjMSfrzMETfp7PT5/Y8E+I6yL2Quwrump3mxx8gHlYhcenjMNHd
+5p857u9+8NYI9wCA6ns5dPZ6HJRiyMxvZQKl2F/V4HjXTCbFE93EHN6V7SB7tCEC
+AwEAAaNnMGUwCQYDVR0TBAIwADALBgNVHQ8EBAMCBaAwHQYDVR0lBBYwFAYIKwYB
+BQUHAwEGCCsGAQUFBwMCMCwGA1UdEQQlMCOCCWxvY2FsaG9zdIcEfwAAAYcQAAAA
+AAAAAAAAAAAAAAAAATANBgkqhkiG9w0BAQsFAAOCAgEAjqKFK6HPKqIsGY+ujqcJ
+EjBT3joXJZscfug8VOSShVrouxPvjNG+YEPVVgnXH1s6WD0XeP2QxDLwPvvFftVH
+xYeA3LPB93lvDZ3aCV9c+Vgpu8+iX+JDQ6e0rJ+B73ux3kEl58InCO/iO1gG77K5
+XjEUi77wXdcJ1cyOLF2KWiGaCyNkLKaqj43hzPIWaOLU18eIePhX3nyQl3pEOBYw
+D0wW6dq+5Wq9TdepTj3Karlc1zuS+OusB8TP5UmqmUmZdadChn5AC8DjX0UMppV5
+cALtslb4BU0cWXN0fvEX0gkfZVV7DzWRSWCeTbuBZXkI5DZeSxNVCsfa4grtD/dl
+SbhhhZ9GYOdPig+u24EGPAAYvQUDO9JP81lCH7rS7vc3ZJqIZ4o0pdcclGT6Ml5c
+tPxMcO5AM++C1qGZeB9qP3aeSPzMZzLPfCxuvDzJAAr1D8seOU9dmJ94hdC8lr+J
+FLoiA7ifyc08mVfyg7Ob/E5O6claSiH/pf7c4H8X1LFDjHteO8zbZQbF2dukbt+C
+AQQ22PLFXqG8yXAFSTbBZZBwMWvCjdnlqtbjlmzkVhA/WYJSjMWHk4GG5aDWhYc6
+zKuARQn1sSjf4k+Tz/kUvOJhJ/VFS75hVvB+vlHTiHck76QbZemeqp2UX+8hWzX4
+CYtGXLjjZjsQD7IPorjhW1o=
 -----END CERTIFICATE-----
diff --git a/asterixdb/asterix-app/src/test/resources/security/cc/cc.jks b/asterixdb/asterix-app/src/test/resources/security/cc/cc.jks
index 7aca187..6bcbdc5 100644
--- a/asterixdb/asterix-app/src/test/resources/security/cc/cc.jks
+++ b/asterixdb/asterix-app/src/test/resources/security/cc/cc.jks
Binary files differ
diff --git a/asterixdb/asterix-app/src/test/resources/security/cc/cc.p12 b/asterixdb/asterix-app/src/test/resources/security/cc/cc.p12
index 751bb8e..656d2b9 100644
--- a/asterixdb/asterix-app/src/test/resources/security/cc/cc.p12
+++ b/asterixdb/asterix-app/src/test/resources/security/cc/cc.p12
Binary files differ
diff --git a/asterixdb/asterix-app/src/test/resources/security/nc1/asterix_nc1.crt b/asterixdb/asterix-app/src/test/resources/security/nc1/asterix_nc1.crt
index 8fbff0a..b871286 100644
--- a/asterixdb/asterix-app/src/test/resources/security/nc1/asterix_nc1.crt
+++ b/asterixdb/asterix-app/src/test/resources/security/nc1/asterix_nc1.crt
@@ -1,23 +1,25 @@
 -----BEGIN CERTIFICATE-----
-MIIDyjCCAbICCQDz/BMGga4kNjANBgkqhkiG9w0BAQsFADApMQswCQYDVQQGEwJY
-WDEaMBgGA1UEAwwRQXN0ZXJpeERCIFRlc3QgQ0EwHhcNMTgxMTIxMTMyODIwWhcN
-MjgxMTE4MTMyODIwWjAlMQswCQYDVQQGEwJYWDEWMBQGA1UEAwwNYXN0ZXJpeGRi
-X25jMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALM0qmtXFlDUcwly
-VT3MjWy1SokSxxjqPK2Y/zWmeupzCXZbP53N3kryAcO5sB/+aMgaYnKWCYNl6dlC
-GwQSzzwkI7hesyHlU7EiUeG920w9XJ7EdVnE/+kwtRevN6UOondzz0NlusLaHeJE
-Vj7kD7pP4LHRMUhSN2vANWQBjzs+wGcEn5li+i8goCI96KFH7j7M1GoDmFHIC0IQ
-9v9QHYQ/rUu2kbeqlyAIY7ZWURGIQgL5IiKLAa4TVHiSdDj911kZMiAgpQouotSs
-5zPMjj0KuusoC0zsCLaj8yJKZ8gscKTW0Ny48CQoR1j15FVwl7PNh2uLLNdc/ZVv
-DSUJgjMCAwEAATANBgkqhkiG9w0BAQsFAAOCAgEAozI3Up3/8oBVlKsxOWG5b98l
-isVcjO2rvwWZNy3ZI8bGma2TymBAeMQMxemXWUaWkeajJt0MzWD7tDaDZJyz1kbV
-E3dNDEdPT1I+xReQNadRiTdN3qyN49PG4eexBoYaYzvlrxmYZH632QwVOwYiyjRM
-qbzZP5k4fOLEwkPrR38ghJOFuI49QxVbC4+QJ7EgjuC78ZmB39QuKZIXiYMGojRl
-ItF95Y/dNTZYzXZnJTbeGylycyO4mkd9b6bRtsN79toate4tzUpm15l5B3ZkD0dX
-Lc4sKPBGAGqwrHojC6ipyyJpJIQfP/VoWUs+FdezA45+AHMkXZqiPhYBOqA66y1X
-z8NJDi87rcefDTwfFvqyyuodlOBPrFcNSowE/VZw51HpyAxkmYeObrJVk087Iz8i
-UrKMgrQ+w0hyA7IW6ma7ezC8607MMMTOB+FngX5CDcOjzAGPLpi/bgAho7CenmFG
-GYKKyWYW4FTnsiNH7Fi/WYKliKEBTmDBVt8crFHwDamLfMyEKvQExoCCgAu+MSs1
-+Yd0JFQ0OOELMjyo2UYt7A6exYSZ57TnqmGRM2QMwRtvwafOzqPL2xwtUZxdgphu
-XxijoZo5eA/DIENSLA0l2B+WaBh+MkdySuxqi2CBAxgldTQyUgsiyu7ctsYIMdMa
-nVIrPLaI1domgvyoXSA=
+MIIEODCCAiCgAwIBAgIJAOor3RjfqKKwMA0GCSqGSIb3DQEBCwUAMCkxCzAJBgNV
+BAYTAlhYMRowGAYDVQQDDBFBc3Rlcml4REIgVGVzdCBDQTAeFw0yNjA5MTYwMDIy
+MDFaFw0zNjA5MTMwMDIyMDFaMCUxCzAJBgNVBAYTAlhYMRYwFAYDVQQDDA1hc3Rl
+cml4ZGJfbmMxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAszSqa1cW
+UNRzCXJVPcyNbLVKiRLHGOo8rZj/NaZ66nMJdls/nc3eSvIBw7mwH/5oyBpicpYJ
+g2Xp2UIbBBLPPCQjuF6zIeVTsSJR4b3bTD1cnsR1WcT/6TC1F683pQ6id3PPQ2W6
+wtod4kRWPuQPuk/gsdExSFI3a8A1ZAGPOz7AZwSfmWL6LyCgIj3ooUfuPszUagOY
+UcgLQhD2/1AdhD+tS7aRt6qXIAhjtlZREYhCAvkiIosBrhNUeJJ0OP3XWRkyICCl
+Ci6i1KznM8yOPQq66ygLTOwItqPzIkpnyCxwpNbQ3LjwJChHWPXkVXCXs82Ha4ss
+11z9lW8NJQmCMwIDAQABo2cwZTAJBgNVHRMEAjAAMAsGA1UdDwQEAwIFoDAdBgNV
+HSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwLAYDVR0RBCUwI4IJbG9jYWxob3N0
+hwR/AAABhxAAAAAAAAAAAAAAAAAAAAABMA0GCSqGSIb3DQEBCwUAA4ICAQAUWJ6w
+pnPGT0wxhU0U8jbw7kJhPV+nCdC7irqz3vYE2jbTlwilxhMHsuUt8xr4G25DTkhR
+u52ZyFVCyQBVGujXsjTVLPo99fiHRssrjRBJLTPRRibg9+Q/qUgMBlelUvPBeF7h
+3/oDJjrN9s6LPHQeTceSqRB3fuRBg0e35rNrDxEv+uhmPNMEPEonmJ8Js63ignyQ
+SJT/PtUBUfFCdg/IrOWH3xC/nr4AW7p5wgCR+bLSNBObMqcuYIPoL1R9zLfZ/AFK
+NmgItmlrsMA99+OY7XkJF/oLQTBEDG+cwfQe6eNwtoTJo7pgZO4wkhi8oLqb1zEo
+/NkrH6F0j3dkiEgX+cIY+sOngdCQ8jkUf3H53ZqobvAYujyK+Npc4mb2JNRNMZRr
+PPufslGqcxuRmAfgPQ9QjaYvgcr8kQbh6gyho3OVp4XNuuA9up7JSlIdUz8/nrdR
+OXvV4NADXakmQ608tVxS6iILPtzUkLpfgqtEbkRmk/a/s71G+4DGD8pHH8gCPxxo
+lTzTntmDNt6GT+XGg8LL4q/1MF0Vr0X6htyQWbsBomk8id3o8f/qHGUrEGiGgDSh
+XD+MCouti9QQ3bkI+qLR2YqHCl2GVk0LBz86yEWWJUlrfcMujhxSH8vUjV//jpND
+0p7PM8HH8O92fLHolbWHViGeKmCEjnGBmzZs8A==
 -----END CERTIFICATE-----
diff --git a/asterixdb/asterix-app/src/test/resources/security/nc1/asterix_nc1.jks b/asterixdb/asterix-app/src/test/resources/security/nc1/asterix_nc1.jks
index ecbde7e..a186509 100644
--- a/asterixdb/asterix-app/src/test/resources/security/nc1/asterix_nc1.jks
+++ b/asterixdb/asterix-app/src/test/resources/security/nc1/asterix_nc1.jks
Binary files differ
diff --git a/asterixdb/asterix-app/src/test/resources/security/nc1/asterix_nc1.p12 b/asterixdb/asterix-app/src/test/resources/security/nc1/asterix_nc1.p12
index 0736eae..6255394 100644
--- a/asterixdb/asterix-app/src/test/resources/security/nc1/asterix_nc1.p12
+++ b/asterixdb/asterix-app/src/test/resources/security/nc1/asterix_nc1.p12
Binary files differ
diff --git a/asterixdb/asterix-app/src/test/resources/security/nc2/asterix_nc2.crt b/asterixdb/asterix-app/src/test/resources/security/nc2/asterix_nc2.crt
index 486d2a0..c0eda85 100644
--- a/asterixdb/asterix-app/src/test/resources/security/nc2/asterix_nc2.crt
+++ b/asterixdb/asterix-app/src/test/resources/security/nc2/asterix_nc2.crt
@@ -1,23 +1,25 @@
 -----BEGIN CERTIFICATE-----
-MIIDyjCCAbICCQDz/BMGga4kNzANBgkqhkiG9w0BAQsFADApMQswCQYDVQQGEwJY
-WDEaMBgGA1UEAwwRQXN0ZXJpeERCIFRlc3QgQ0EwHhcNMTgxMTIxMTMyODQ4WhcN
-MjgxMTE4MTMyODQ4WjAlMQswCQYDVQQGEwJYWDEWMBQGA1UEAwwNYXN0ZXJpeGRi
-X25jMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMVL+K/tP23E6Lk6
-uZH38yzKSEdAxDgnxAMVqTiqgEz8lz1YvFsY43rlBLu0+NSbNTRlUXhoxfFRVKN5
-e/iLVPFWL9VViLZP2FvTY0z6pkdwjKHtLA1uCMtUyIZwjB9yv7k0uVmvqm/zmFpm
-riL4TZA3/cNGZZw6MewkT0LeqI5vHUWfsNmDqYRyDucFOUVNunneZ6TZPMaspqOg
-XpwdN4UkbC2TUnWjMxC/XB+ZhkOAIYNYIvXzsL15tRV/3Rba5lJZHg7KaxM3rPo3
-8mmtA/y4rtlqouYf8X2nSbol3+WX3/U7xXtc2YuB5WUlXtUyixO8E8jVadpTUot4
-fU7CNOsCAwEAATANBgkqhkiG9w0BAQsFAAOCAgEAcIbcRE813t3Deku4K1867YBG
-sQ+sSshLy2VRWIykI7S9KRQwP2VuixvuUw/U7Ey39fDzj1howMhulFq8jXOQKp1z
-Fk2dmA5cDlAxEyD42k8QgvM8G9Kz6dmv4eV3cnUqv6f1VA3wV/598y3hhPuLHCXx
-vAaoGBCgdokDAARbirBU4UBjBO/Frjo63L6CoR3Avy3Cm+Hk556cTQlfWTMOKuOR
-wUINusnLc2x5q0tN7CSkZKzjyfotTSle2cfupnTLlVKC3/ln1Thga6gCkht39FtE
-XVWj1A2qNQlud5RTkmTLkedLhkRXvMSdD/V2pxo5gFaYTNRBHEGUhpXyOSnImLoh
-Uc7LFfalHC2cvhqNJNd4VPA7PNjNO6dvd/oNVclIFlrtiEMbTXnsLR8Tl7DU/L1/
-uxnYsdJomMTyFpCxMTaqDuYmsRKwcqaR0LQKvEmIWAzomjg+o5vpnhsThwpLBemA
-cboOFXV7mZiV4wLXzTUMnYHlb8syoNtliVbG+fkePVzZRkjD5wHW6OQbIJwE8/lB
-oY/XRsga9NT0O6KVfaLLJjeDRNLXBh+d7SjVPERIiut1/WnRVADSn/5g45YTWifM
-7UTw68DtCUA9HdXuNZ3d1B2zyPncNd2utJA6PIqVVDPGi0F0w4h1blSY/x5aOTjo
-VZ5xVqVAqZ2+K5Ljjvs=
+MIIEODCCAiCgAwIBAgIJAOor3RjfqKKxMA0GCSqGSIb3DQEBCwUAMCkxCzAJBgNV
+BAYTAlhYMRowGAYDVQQDDBFBc3Rlcml4REIgVGVzdCBDQTAeFw0yNjA5MTYwMDIy
+MDFaFw0zNjA5MTMwMDIyMDFaMCUxCzAJBgNVBAYTAlhYMRYwFAYDVQQDDA1hc3Rl
+cml4ZGJfbmMyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxUv4r+0/
+bcTouTq5kffzLMpIR0DEOCfEAxWpOKqATPyXPVi8WxjjeuUEu7T41Js1NGVReGjF
+8VFUo3l7+ItU8VYv1VWItk/YW9NjTPqmR3CMoe0sDW4Iy1TIhnCMH3K/uTS5Wa+q
+b/OYWmauIvhNkDf9w0ZlnDox7CRPQt6ojm8dRZ+w2YOphHIO5wU5RU26ed5npNk8
+xqymo6BenB03hSRsLZNSdaMzEL9cH5mGQ4Ahg1gi9fOwvXm1FX/dFtrmUlkeDspr
+Ezes+jfyaa0D/Liu2Wqi5h/xfadJuiXf5Zff9TvFe1zZi4HlZSVe1TKLE7wTyNVp
+2lNSi3h9TsI06wIDAQABo2cwZTAJBgNVHRMEAjAAMAsGA1UdDwQEAwIFoDAdBgNV
+HSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwLAYDVR0RBCUwI4IJbG9jYWxob3N0
+hwR/AAABhxAAAAAAAAAAAAAAAAAAAAABMA0GCSqGSIb3DQEBCwUAA4ICAQAaQO7L
+ZobeU6wsvLlTwIgyOpUB5uoPEQn6tH/gi2gG5tFe2DahFV9Nl5DEfbjEpMmo9aVP
+DSe1WKGTqQcnM9rXltkbas+Hs7lNNW2DcIHA+nnN/gsjOjvaQdZtMgDAmQfTNb5J
+ObCikrz5DGEkQrMv1oEd+OkJIZeZ8YLY5IsddUIuUtDyvqu0IWeJ00COCSMlfUJf
+wP6A2Uu6EITCe2tLALQcHj80s3Lv2Ut2NzGFFt0gd/Xcjoy1lf1NV50GdagTlxM7
+HI/vE1wEDKJa3d2EQ1La84bMeFxYIOMWh9GXZvTEGMoLFrROw4TAl7wBhqfL88EQ
+m+ryR2LZS44jdzsLxHXgjwuITISMAiBB6Z/p9ozAYj4VbxrI4IJcD6++9gdW9KSv
+oQJtU+UxQOghVYeZQQTF25H2C/Wz287M34/OgGQwxCcTjlf0aplDwipNECwqo/+0
+KHPXlYbZ4yaIav/jWI4PNlsfIEyeicefBNTUslo1r7qpSQmMZvnT8oPmLiGOgQM4
+NtXSWy/ucrnPzIbYBhc6H3uIyBkeJllfmIZyiOz2VppiZXFX+X7HN8/TiqAKYyM9
+GRCIJFNvAac0wwmBt5Zt10rNIkccflsvmTSfmZsoen6BTe3radcy+ejNMeErd/1j
+aFcTVmk3iK67yIGR8YizSp6YVir3lpaX2oeELg==
 -----END CERTIFICATE-----
diff --git a/asterixdb/asterix-app/src/test/resources/security/nc2/asterix_nc2.jks b/asterixdb/asterix-app/src/test/resources/security/nc2/asterix_nc2.jks
index df4f83f..d5c4378 100644
--- a/asterixdb/asterix-app/src/test/resources/security/nc2/asterix_nc2.jks
+++ b/asterixdb/asterix-app/src/test/resources/security/nc2/asterix_nc2.jks
Binary files differ
diff --git a/asterixdb/asterix-app/src/test/resources/security/nc2/asterix_nc2.p12 b/asterixdb/asterix-app/src/test/resources/security/nc2/asterix_nc2.p12
index 4839db9..61fbcb4 100644
--- a/asterixdb/asterix-app/src/test/resources/security/nc2/asterix_nc2.p12
+++ b/asterixdb/asterix-app/src/test/resources/security/nc2/asterix_nc2.p12
Binary files differ
diff --git a/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/RMIClientFactory.java b/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/RMIClientFactory.java
index ce459e2..55c393c 100644
--- a/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/RMIClientFactory.java
+++ b/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/RMIClientFactory.java
@@ -26,6 +26,8 @@
 import java.rmi.server.RMIClientSocketFactory;
 
 import javax.net.SocketFactory;
+import javax.net.ssl.SSLParameters;
+import javax.net.ssl.SSLSocket;
 import javax.net.ssl.SSLSocketFactory;
 
 import org.apache.hyracks.api.network.INetworkSecurityConfig;
@@ -66,30 +68,48 @@
     private static class RMITrustedClientSSLSocketFactory extends SSLSocketFactory {
 
         protected SSLSocketFactory factory;
+        private final boolean verifyPeerIdentity;
 
         public RMITrustedClientSSLSocketFactory(INetworkSecurityConfig config) {
             this.factory = NetworkSecurityManager.newSSLContext(config).getSocketFactory();
+            this.verifyPeerIdentity = config.verifyRmiPeerIdentity();
         }
 
         public Socket createSocket(InetAddress host, int port) throws IOException {
-            return this.factory.createSocket(host, port);
+            return identifyPeer(this.factory.createSocket(host, port));
         }
 
         public Socket createSocket(String host, int port) throws IOException {
-            return this.factory.createSocket(host, port);
+            return identifyPeer(this.factory.createSocket(host, port));
         }
 
         public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException {
-            return this.factory.createSocket(host, port, localHost, localPort);
+            return identifyPeer(this.factory.createSocket(host, port, localHost, localPort));
         }
 
         public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort)
                 throws IOException {
-            return this.factory.createSocket(address, port, localAddress, localPort);
+            return identifyPeer(this.factory.createSocket(address, port, localAddress, localPort));
         }
 
         public Socket createSocket(Socket socket, String host, int port, boolean autoClose) throws IOException {
-            return this.factory.createSocket(socket, host, port, autoClose);
+            return identifyPeer(this.factory.createSocket(socket, host, port, autoClose));
+        }
+
+        /**
+         * Requires the certificate the peer presents to identify the host this socket was opened to. An
+         * {@link SSLSocket} performs no such check unless it is asked to, leaving any holder of a certificate the
+         * trust store chains to able to answer in place of the metadata node.
+         */
+        private Socket identifyPeer(Socket socket) {
+            if (!verifyPeerIdentity) {
+                return socket;
+            }
+            SSLSocket sslSocket = (SSLSocket) socket;
+            SSLParameters sslParameters = sslSocket.getSSLParameters();
+            sslParameters.setEndpointIdentificationAlgorithm(NetworkSecurityManager.ENDPOINT_IDENTIFICATION_ALGORITHM);
+            sslSocket.setSSLParameters(sslParameters);
+            return sslSocket;
         }
 
         public String[] getDefaultCipherSuites() {
diff --git a/hyracks-fullstack/hyracks/hyracks-api/src/main/java/org/apache/hyracks/api/network/INetworkSecurityConfig.java b/hyracks-fullstack/hyracks/hyracks-api/src/main/java/org/apache/hyracks/api/network/INetworkSecurityConfig.java
index 7fc0335..d71f188 100644
--- a/hyracks-fullstack/hyracks/hyracks-api/src/main/java/org/apache/hyracks/api/network/INetworkSecurityConfig.java
+++ b/hyracks-fullstack/hyracks/hyracks-api/src/main/java/org/apache/hyracks/api/network/INetworkSecurityConfig.java
@@ -34,6 +34,28 @@
     boolean isSslEnabled();
 
     /**
+     * Indicates if the certificate a peer presents on a secured cluster connection must identify the address that
+     * peer was reached at. When this is false the peer's chain is still validated against the trust store, but any
+     * host holding a certificate it validates is accepted in place of the node that was dialled.
+     *
+     * @return true if the peer's identity should be verified. Otherwise false.
+     */
+    default boolean verifyPeerIdentity() {
+        return true;
+    }
+
+    /**
+     * As {@link #verifyPeerIdentity()}, for RMI connections. These are separate because RMI reaches a node by the
+     * host recorded in the exported stub ({@code java.rmi.server.hostname}), which is configured independently of
+     * the address the cluster's own connections use and so may be covered by a different name, or none.
+     *
+     * @return true if the peer's identity should be verified on RMI connections. Otherwise false.
+     */
+    default boolean verifyRmiPeerIdentity() {
+        return true;
+    }
+
+    /**
      * Gets the key store to be used for secured connections
      *
      * @return the key store to be used
diff --git a/hyracks-fullstack/hyracks/hyracks-api/src/main/java/org/apache/hyracks/api/network/INetworkSecurityManager.java b/hyracks-fullstack/hyracks/hyracks-api/src/main/java/org/apache/hyracks/api/network/INetworkSecurityManager.java
index eb52436..90bd171 100644
--- a/hyracks-fullstack/hyracks/hyracks-api/src/main/java/org/apache/hyracks/api/network/INetworkSecurityManager.java
+++ b/hyracks-fullstack/hyracks/hyracks-api/src/main/java/org/apache/hyracks/api/network/INetworkSecurityManager.java
@@ -18,6 +18,8 @@
  */
 package org.apache.hyracks.api.network;
 
+import java.net.InetSocketAddress;
+
 import javax.net.ssl.SSLContext;
 import javax.net.ssl.SSLEngine;
 
@@ -31,11 +33,23 @@
     SSLContext newSSLContext();
 
     /**
-     * Creates a new ssl engine based on the current configuration of this {@link INetworkSecurityManager}
+     * Creates a new server-mode ssl engine based on the current configuration of this
+     * {@link INetworkSecurityManager}
      *
      * @return a new ssl engine
      */
-    SSLEngine newSSLEngine();
+    SSLEngine newServerSSLEngine();
+
+    /**
+     * Creates a new client-mode ssl engine based on the current configuration of this
+     * {@link INetworkSecurityManager}. The engine verifies that the certificate the peer presents identifies
+     * {@code peer}; validating the chain against the trust store on its own leaves any holder of a trusted
+     * certificate able to answer in place of the node that was dialled.
+     *
+     * @param peer the address this connection was made to
+     * @return a new ssl engine
+     */
+    SSLEngine newClientSSLEngine(InetSocketAddress peer);
 
     /**
      * Sets the configuration to be used for this {@link INetworkSecurityManager}
diff --git a/hyracks-fullstack/hyracks/hyracks-control/hyracks-control-cc/src/main/java/org/apache/hyracks/control/cc/ClusterControllerService.java b/hyracks-fullstack/hyracks/hyracks-control/hyracks-control-cc/src/main/java/org/apache/hyracks/control/cc/ClusterControllerService.java
index 1dd54ad..47bdc1c 100644
--- a/hyracks-fullstack/hyracks/hyracks-control/hyracks-control-cc/src/main/java/org/apache/hyracks/control/cc/ClusterControllerService.java
+++ b/hyracks-fullstack/hyracks/hyracks-control/hyracks-control-cc/src/main/java/org/apache/hyracks/control/cc/ClusterControllerService.java
@@ -571,7 +571,8 @@
 
     protected INetworkSecurityManager createNetworkSecurityManager(IApplicationConfig appConfig, IApplication app)
             throws Exception {
-        return new NetworkSecurityManager(NetworkSecurityConfig.of(ccConfig.isSslEnabled(), ccConfig.getKeyStorePath(),
-                ccConfig.getKeyStorePassword(), ccConfig.getTrustStorePath()));
+        return new NetworkSecurityManager(NetworkSecurityConfig.of(ccConfig.isSslEnabled(),
+                ccConfig.isSslClusterVerifyPeerIdentity(), ccConfig.isSslRmiVerifyPeerIdentity(),
+                ccConfig.getKeyStorePath(), ccConfig.getKeyStorePassword(), ccConfig.getTrustStorePath()));
     }
 }
diff --git a/hyracks-fullstack/hyracks/hyracks-control/hyracks-control-common/src/main/java/org/apache/hyracks/control/common/controllers/ControllerConfig.java b/hyracks-fullstack/hyracks/hyracks-control/hyracks-control-common/src/main/java/org/apache/hyracks/control/common/controllers/ControllerConfig.java
index 19eba57..16d938a 100644
--- a/hyracks-fullstack/hyracks/hyracks-control/hyracks-control-common/src/main/java/org/apache/hyracks/control/common/controllers/ControllerConfig.java
+++ b/hyracks-fullstack/hyracks/hyracks-control/hyracks-control-common/src/main/java/org/apache/hyracks/control/common/controllers/ControllerConfig.java
@@ -50,7 +50,24 @@
                 (Function<IApplicationConfig, String>) appConfig -> FileUtil
                         .joinPath(appConfig.getString(ControllerConfig.Option.DEFAULT_DIR), "logs"),
                 "The directory where logs for this node are written"),
-        SSL_ENABLED(BOOLEAN, false, "A flag indicating if cluster communications should use secured connections");
+        SSL_ENABLED(BOOLEAN, false, "A flag indicating if cluster communications should use secured connections"),
+        SSL_VERIFY_PEER_IDENTITY(
+                BOOLEAN,
+                true,
+                "A flag indicating if the certificate a peer presents on a secured connection must identify the"
+                        + " address it was reached at. Disabling this accepts any certificate the trust store"
+                        + " validates, whichever host presents it. This is the default for each of the connections"
+                        + " below, which may also be set on their own"),
+        SSL_CLUSTER_VERIFY_PEER_IDENTITY(
+                BOOLEAN,
+                SSL_VERIFY_PEER_IDENTITY,
+                "A flag indicating if the certificate a peer presents on a secured cluster connection must identify"
+                        + " the address that peer was reached at"),
+        SSL_RMI_VERIFY_PEER_IDENTITY(
+                BOOLEAN,
+                SSL_VERIFY_PEER_IDENTITY,
+                "A flag indicating if the certificate presented on a secured RMI connection must identify the"
+                        + " address it was reached at");
 
         private final IOptionType type;
         private final String description;
@@ -68,6 +85,12 @@
             this.description = description;
         }
 
+        <T> Option(IOptionType<T> type, Option defaultOption, String description) {
+            this.type = type;
+            this.defaultValue = defaultOption;
+            this.description = description;
+        }
+
         @Override
         public Section section() {
             return Section.COMMON;
@@ -133,4 +156,15 @@
         return getAppConfig().getBoolean(Option.SSL_ENABLED);
     }
 
+    public boolean isSslVerifyPeerIdentity() {
+        return getAppConfig().getBoolean(Option.SSL_VERIFY_PEER_IDENTITY);
+    }
+
+    public boolean isSslClusterVerifyPeerIdentity() {
+        return getAppConfig().getBoolean(Option.SSL_CLUSTER_VERIFY_PEER_IDENTITY);
+    }
+
+    public boolean isSslRmiVerifyPeerIdentity() {
+        return getAppConfig().getBoolean(Option.SSL_RMI_VERIFY_PEER_IDENTITY);
+    }
 }
diff --git a/hyracks-fullstack/hyracks/hyracks-control/hyracks-control-nc/src/main/java/org/apache/hyracks/control/nc/NodeControllerService.java b/hyracks-fullstack/hyracks/hyracks-control/hyracks-control-nc/src/main/java/org/apache/hyracks/control/nc/NodeControllerService.java
index 90d4255..59b2800 100644
--- a/hyracks-fullstack/hyracks/hyracks-control/hyracks-control-nc/src/main/java/org/apache/hyracks/control/nc/NodeControllerService.java
+++ b/hyracks-fullstack/hyracks/hyracks-control/hyracks-control-nc/src/main/java/org/apache/hyracks/control/nc/NodeControllerService.java
@@ -729,7 +729,8 @@
 
     protected INetworkSecurityManager createNetworkSecurityManager(IApplicationConfig appConfig,
             INCApplication application) {
-        return new NetworkSecurityManager(NetworkSecurityConfig.of(ncConfig.isSslEnabled(), ncConfig.getKeyStorePath(),
-                ncConfig.getKeyStorePassword(), ncConfig.getTrustStorePath()));
+        return new NetworkSecurityManager(NetworkSecurityConfig.of(ncConfig.isSslEnabled(),
+                ncConfig.isSslClusterVerifyPeerIdentity(), ncConfig.isSslRmiVerifyPeerIdentity(),
+                ncConfig.getKeyStorePath(), ncConfig.getKeyStorePassword(), ncConfig.getTrustStorePath()));
     }
 }
diff --git a/hyracks-fullstack/hyracks/hyracks-ipc/pom.xml b/hyracks-fullstack/hyracks/hyracks-ipc/pom.xml
index 35ebde2..00fd85f 100644
--- a/hyracks-fullstack/hyracks/hyracks-ipc/pom.xml
+++ b/hyracks-fullstack/hyracks/hyracks-ipc/pom.xml
@@ -82,5 +82,17 @@
       <groupId>org.apache.httpcomponents</groupId>
       <artifactId>httpclient</artifactId>
     </dependency>
+    <dependency>
+      <groupId>org.bouncycastle</groupId>
+      <artifactId>bcpkix-jdk18on</artifactId>
+      <version>1.85</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.bouncycastle</groupId>
+      <artifactId>bcprov-jdk18on</artifactId>
+      <version>1.85.2</version>
+      <scope>test</scope>
+    </dependency>
   </dependencies>
 </project>
diff --git a/hyracks-fullstack/hyracks/hyracks-ipc/src/main/java/org/apache/hyracks/ipc/security/NetworkSecurityConfig.java b/hyracks-fullstack/hyracks/hyracks-ipc/src/main/java/org/apache/hyracks/ipc/security/NetworkSecurityConfig.java
index bfcd623..ca96271 100644
--- a/hyracks-fullstack/hyracks/hyracks-ipc/src/main/java/org/apache/hyracks/ipc/security/NetworkSecurityConfig.java
+++ b/hyracks-fullstack/hyracks/hyracks-ipc/src/main/java/org/apache/hyracks/ipc/security/NetworkSecurityConfig.java
@@ -36,15 +36,20 @@
     private static final long serialVersionUID = 2L;
     private static final char[] INTEGRITY_PASSWORD = NetworkSecurityConfig.class.getName().toCharArray();
     private final boolean sslEnabled;
+    private final boolean verifyPeerIdentity;
+    private final boolean verifyRmiPeerIdentity;
     private final File keyStoreFile;
     private final File trustStoreFile;
     private final String keyStorePassword;
     private transient KeyStore keyStore;
     private transient KeyStore trustStore;
 
-    private NetworkSecurityConfig(boolean sslEnabled, String keyStoreFile, String keyStorePassword,
-            String trustStoreFile, KeyStore keyStore, KeyStore trustStore) {
+    private NetworkSecurityConfig(boolean sslEnabled, boolean verifyPeerIdentity, boolean verifyRmiPeerIdentity,
+            String keyStoreFile, String keyStorePassword, String trustStoreFile, KeyStore keyStore,
+            KeyStore trustStore) {
         this.sslEnabled = sslEnabled;
+        this.verifyPeerIdentity = verifyPeerIdentity;
+        this.verifyRmiPeerIdentity = verifyRmiPeerIdentity;
         this.keyStoreFile = keyStoreFile != null ? new File(keyStoreFile) : null;
         this.keyStorePassword = keyStorePassword;
         this.trustStoreFile = trustStoreFile != null ? new File(trustStoreFile) : null;
@@ -54,12 +59,24 @@
 
     public static NetworkSecurityConfig of(boolean sslEnabled, String keyStoreFile, String keyStorePassword,
             String trustStoreFile) {
-        return new NetworkSecurityConfig(sslEnabled, keyStoreFile, keyStorePassword, trustStoreFile, null, null);
+        return of(sslEnabled, true, true, keyStoreFile, keyStorePassword, trustStoreFile);
+    }
+
+    public static NetworkSecurityConfig of(boolean sslEnabled, boolean verifyPeerIdentity,
+            boolean verifyRmiPeerIdentity, String keyStoreFile, String keyStorePassword, String trustStoreFile) {
+        return new NetworkSecurityConfig(sslEnabled, verifyPeerIdentity, verifyRmiPeerIdentity, keyStoreFile,
+                keyStorePassword, trustStoreFile, null, null);
     }
 
     public static NetworkSecurityConfig of(boolean sslEnabled, KeyStore keyStore, String keyStorePassword,
             KeyStore trustStore) {
-        return new NetworkSecurityConfig(sslEnabled, null, keyStorePassword, null, keyStore, trustStore);
+        return of(sslEnabled, true, true, keyStore, keyStorePassword, trustStore);
+    }
+
+    public static NetworkSecurityConfig of(boolean sslEnabled, boolean verifyPeerIdentity,
+            boolean verifyRmiPeerIdentity, KeyStore keyStore, String keyStorePassword, KeyStore trustStore) {
+        return new NetworkSecurityConfig(sslEnabled, verifyPeerIdentity, verifyRmiPeerIdentity, null, keyStorePassword,
+                null, keyStore, trustStore);
     }
 
     @Override
@@ -68,6 +85,16 @@
     }
 
     @Override
+    public boolean verifyPeerIdentity() {
+        return verifyPeerIdentity;
+    }
+
+    @Override
+    public boolean verifyRmiPeerIdentity() {
+        return verifyRmiPeerIdentity;
+    }
+
+    @Override
     public File getKeyStoreFile() {
         return keyStoreFile;
     }
diff --git a/hyracks-fullstack/hyracks/hyracks-ipc/src/main/java/org/apache/hyracks/ipc/security/NetworkSecurityManager.java b/hyracks-fullstack/hyracks/hyracks-ipc/src/main/java/org/apache/hyracks/ipc/security/NetworkSecurityManager.java
index db524ca..f6a35c4 100644
--- a/hyracks-fullstack/hyracks/hyracks-ipc/src/main/java/org/apache/hyracks/ipc/security/NetworkSecurityManager.java
+++ b/hyracks-fullstack/hyracks/hyracks-ipc/src/main/java/org/apache/hyracks/ipc/security/NetworkSecurityManager.java
@@ -19,12 +19,14 @@
 package org.apache.hyracks.ipc.security;
 
 import java.io.FileInputStream;
+import java.net.InetSocketAddress;
 import java.security.KeyStore;
 import java.security.SecureRandom;
 
 import javax.net.ssl.KeyManagerFactory;
 import javax.net.ssl.SSLContext;
 import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLParameters;
 import javax.net.ssl.TrustManagerFactory;
 
 import org.apache.hyracks.api.network.INetworkSecurityConfig;
@@ -38,6 +40,7 @@
     private volatile INetworkSecurityConfig config;
     protected final ISocketChannelFactory sslSocketFactory;
     public static final String TSL_VERSION = "TLSv1.2";
+    public static final String ENDPOINT_IDENTIFICATION_ALGORITHM = "HTTPS";
 
     public NetworkSecurityManager(INetworkSecurityConfig config) {
         this.config = config;
@@ -50,10 +53,29 @@
     }
 
     @Override
-    public SSLEngine newSSLEngine() {
+    public SSLEngine newServerSSLEngine() {
         try {
-            SSLContext ctx = newSSLContext();
-            return ctx.createSSLEngine();
+            SSLEngine sslEngine = newSSLContext().createSSLEngine();
+            sslEngine.setUseClientMode(false);
+            return sslEngine;
+        } catch (Exception ex) {
+            throw new IllegalStateException("Failed to create SSLEngine", ex);
+        }
+    }
+
+    @Override
+    public SSLEngine newClientSSLEngine(InetSocketAddress peer) {
+        try {
+            // the peer host is the name matched against the certificate, so an engine created without it cannot
+            // identify anything- the handshake then fails outright rather than skipping the check
+            SSLEngine sslEngine = newSSLContext().createSSLEngine(peer.getHostString(), peer.getPort());
+            sslEngine.setUseClientMode(true);
+            if (config.verifyPeerIdentity()) {
+                SSLParameters sslParameters = sslEngine.getSSLParameters();
+                sslParameters.setEndpointIdentificationAlgorithm(ENDPOINT_IDENTIFICATION_ALGORITHM);
+                sslEngine.setSSLParameters(sslParameters);
+            }
+            return sslEngine;
         } catch (Exception ex) {
             throw new IllegalStateException("Failed to create SSLEngine", ex);
         }
diff --git a/hyracks-fullstack/hyracks/hyracks-ipc/src/main/java/org/apache/hyracks/ipc/sockets/SslSocketChannelFactory.java b/hyracks-fullstack/hyracks/hyracks-ipc/src/main/java/org/apache/hyracks/ipc/sockets/SslSocketChannelFactory.java
index 2a2fb62..593d87b 100644
--- a/hyracks-fullstack/hyracks/hyracks-ipc/src/main/java/org/apache/hyracks/ipc/sockets/SslSocketChannelFactory.java
+++ b/hyracks-fullstack/hyracks/hyracks-ipc/src/main/java/org/apache/hyracks/ipc/sockets/SslSocketChannelFactory.java
@@ -18,6 +18,10 @@
  */
 package org.apache.hyracks.ipc.sockets;
 
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
 import java.nio.channels.SocketChannel;
 
 import javax.net.ssl.SSLEngine;
@@ -36,15 +40,32 @@
 
     @Override
     public ISocketChannel createServerChannel(SocketChannel socketChannel) {
-        final SSLEngine sslEngine = networkSecurityManager.newSSLEngine();
-        sslEngine.setUseClientMode(false);
+        final SSLEngine sslEngine = networkSecurityManager.newServerSSLEngine();
         return new SslSocketChannel(socketChannel, sslEngine);
     }
 
     @Override
     public ISocketChannel createClientChannel(SocketChannel socketChannel) {
-        final SSLEngine sslEngine = networkSecurityManager.newSSLEngine();
-        sslEngine.setUseClientMode(true);
+        final SSLEngine sslEngine = networkSecurityManager.newClientSSLEngine(peerOf(socketChannel));
         return new SslSocketChannel(socketChannel, sslEngine);
     }
+
+    /**
+     * The address the channel was connected to, which is the name the peer's certificate has to identify. This is
+     * the address passed to {@link SocketChannel#connect}, not a re-resolution of it, so a peer dialled by hostname
+     * is identified by that hostname.
+     */
+    private static InetSocketAddress peerOf(SocketChannel socketChannel) {
+        final SocketAddress remoteAddress;
+        try {
+            remoteAddress = socketChannel.getRemoteAddress();
+        } catch (IOException e) {
+            throw new UncheckedIOException(e);
+        }
+        if (!(remoteAddress instanceof InetSocketAddress)) {
+            // null when the channel is not connected; a client channel is only created once it is
+            throw new IllegalStateException("cannot secure a client channel with remote address " + remoteAddress);
+        }
+        return (InetSocketAddress) remoteAddress;
+    }
 }
diff --git a/hyracks-fullstack/hyracks/hyracks-ipc/src/test/java/org/apache/hyracks/ipc/tests/SslPeerIdentityTest.java b/hyracks-fullstack/hyracks/hyracks-ipc/src/test/java/org/apache/hyracks/ipc/tests/SslPeerIdentityTest.java
new file mode 100644
index 0000000..e4c480f
--- /dev/null
+++ b/hyracks-fullstack/hyracks/hyracks-ipc/src/test/java/org/apache/hyracks/ipc/tests/SslPeerIdentityTest.java
@@ -0,0 +1,238 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.hyracks.ipc.tests;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.math.BigInteger;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.nio.channels.ServerSocketChannel;
+import java.nio.channels.SocketChannel;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.KeyStore;
+import java.security.PrivateKey;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Date;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLException;
+import javax.security.auth.x500.X500Principal;
+
+import org.apache.hyracks.api.network.INetworkSecurityManager;
+import org.apache.hyracks.api.network.ISocketChannel;
+import org.apache.hyracks.ipc.security.NetworkSecurityConfig;
+import org.apache.hyracks.ipc.security.NetworkSecurityManager;
+import org.bouncycastle.asn1.x509.BasicConstraints;
+import org.bouncycastle.asn1.x509.Extension;
+import org.bouncycastle.asn1.x509.GeneralName;
+import org.bouncycastle.asn1.x509.GeneralNames;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
+import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
+import org.bouncycastle.operator.ContentSigner;
+import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+/**
+ * Validating the peer's chain against the trust store says only that a holder of a trusted certificate answered, not
+ * that the node which was dialled did. These assert that a client channel also holds the peer to the identity its
+ * certificate was issued to, so a certificate the cluster CA issued to another node is refused (ASTERIXDB-3851).
+ */
+public class SslPeerIdentityTest {
+
+    private static final String DIALLED_HOST = "localhost";
+    private static final String OTHER_HOST = "hyracks-nc2.example.com";
+    private static final int UNUSED_PORT = 3099;
+    private static final String PASSWORD = SslPeerIdentityTest.class.getSimpleName();
+    private static final String SIGNATURE_ALGORITHM = "SHA256withRSA";
+
+    private static INetworkSecurityManager client;
+    private static INetworkSecurityManager unverifyingClient;
+    private static INetworkSecurityManager dialledHost;
+    private static INetworkSecurityManager otherHost;
+    private static ExecutorService executor;
+
+    @BeforeClass
+    public static void setUp() throws Exception {
+        final KeyPair caKeyPair = keyPair();
+        final X509Certificate ca = certificate(caKeyPair, "Hyracks Test CA", null, caKeyPair.getPrivate(), null);
+        // one CA, trusted by the client, which has issued a certificate to each of two nodes
+        final KeyStore trustStore = emptyStore();
+        trustStore.setCertificateEntry("ca", ca);
+        client = securityManager(emptyStore(), trustStore, true);
+        unverifyingClient = securityManager(emptyStore(), trustStore, false);
+        dialledHost = securityManager(identity(DIALLED_HOST, ca, caKeyPair.getPrivate()), trustStore, true);
+        otherHost = securityManager(identity(OTHER_HOST, ca, caKeyPair.getPrivate()), trustStore, true);
+        executor = Executors.newSingleThreadExecutor();
+    }
+
+    @AfterClass
+    public static void tearDown() {
+        executor.shutdownNow();
+    }
+
+    @Test
+    public void clientEngineIdentifiesThePeerItDialled() {
+        final SSLEngine engine =
+                client.newClientSSLEngine(InetSocketAddress.createUnresolved(DIALLED_HOST, UNUSED_PORT));
+        assertEquals(NetworkSecurityManager.ENDPOINT_IDENTIFICATION_ALGORITHM,
+                engine.getSSLParameters().getEndpointIdentificationAlgorithm());
+        // the algorithm on its own identifies nothing; the name it matches is the peer the engine was created for
+        assertEquals(DIALLED_HOST, engine.getPeerHost());
+        assertEquals(UNUSED_PORT, engine.getPeerPort());
+    }
+
+    /**
+     * A server is not the party doing the dialling, so it has no name to hold its peer to and must not ask for one.
+     */
+    @Test
+    public void serverEngineDoesNotIdentifyTheClient() {
+        assertNull(dialledHost.newServerSSLEngine().getSSLParameters().getEndpointIdentificationAlgorithm());
+    }
+
+    @Test
+    public void certificateIssuedToTheDialledHostIsAccepted() throws Exception {
+        assertTrue(handshake(client, dialledHost));
+    }
+
+    /**
+     * The escape hatch for a deployment whose certificates cannot name the addresses its nodes are reached at. The
+     * chain is still validated, so this is not the same as trusting anything.
+     */
+    @Test
+    public void identityIsNotVerifiedWhenTheOptionIsDisabled() throws Exception {
+        assertNull(unverifyingClient.newClientSSLEngine(InetSocketAddress.createUnresolved(DIALLED_HOST, UNUSED_PORT))
+                .getSSLParameters().getEndpointIdentificationAlgorithm());
+        assertTrue(handshake(unverifyingClient, otherHost));
+    }
+
+    @Test
+    public void certificateIssuedToAnotherHostIsRefused() throws Exception {
+        try {
+            handshake(client, otherHost);
+        } catch (IllegalStateException e) {
+            final SSLException sslFailure = sslCause(e);
+            assertTrue("expected an SSL failure, got " + e, sslFailure != null);
+            assertTrue(sslFailure.getMessage(), sslFailure.getMessage().contains(DIALLED_HOST));
+            return;
+        }
+        throw new AssertionError(
+                "handshake accepted a certificate issued to " + OTHER_HOST + " while dialling " + DIALLED_HOST);
+    }
+
+    /**
+     * Connects to a peer secured by {@code peer} over the loopback interface and completes the client's handshake.
+     * The name the peer's certificate is checked against is the one the socket was opened to, {@link #DIALLED_HOST}.
+     */
+    private boolean handshake(INetworkSecurityManager dialler, INetworkSecurityManager peer) throws Exception {
+        try (ServerSocketChannel listener = ServerSocketChannel.open()) {
+            listener.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0));
+            final int port = ((InetSocketAddress) listener.getLocalAddress()).getPort();
+            try (SocketChannel clientSocket = SocketChannel.open(new InetSocketAddress(DIALLED_HOST, port));
+                    SocketChannel peerSocket = listener.accept()) {
+                clientSocket.configureBlocking(false);
+                peerSocket.configureBlocking(false);
+                final ISocketChannel peerChannel = peer.getSocketChannelFactory().createServerChannel(peerSocket);
+                final ISocketChannel clientChannel =
+                        dialler.getSocketChannelFactory().createClientChannel(clientSocket);
+                // the peer's handshake fails alongside the client's whenever the client refuses its certificate; its
+                // outcome is not what is under test, but it has to be running for the client's to make progress
+                final Future<?> peerHandshake = executor.submit(() -> quietly(peerChannel));
+                try {
+                    return clientChannel.handshake();
+                } finally {
+                    peerHandshake.cancel(true);
+                }
+            }
+        }
+    }
+
+    private static void quietly(ISocketChannel channel) {
+        try {
+            channel.handshake();
+        } catch (Exception e) {
+            // expected whenever the client refuses the certificate
+        }
+    }
+
+    private static SSLException sslCause(Throwable t) {
+        for (Throwable cause = t; cause != null; cause = cause.getCause()) {
+            if (cause instanceof SSLException) {
+                return (SSLException) cause;
+            }
+        }
+        return null;
+    }
+
+    private static INetworkSecurityManager securityManager(KeyStore keyStore, KeyStore trustStore,
+            boolean verifyPeerIdentity) {
+        return new NetworkSecurityManager(
+                NetworkSecurityConfig.of(true, verifyPeerIdentity, verifyPeerIdentity, keyStore, PASSWORD, trustStore));
+    }
+
+    /** A key store holding a certificate issued to {@code host} by {@code ca}, with {@code host} as its only SAN. */
+    private static KeyStore identity(String host, X509Certificate ca, PrivateKey caKey) throws Exception {
+        final KeyPair keyPair = keyPair();
+        final X509Certificate certificate = certificate(keyPair, host, host, caKey, ca);
+        final KeyStore keyStore = emptyStore();
+        keyStore.setKeyEntry("identity", keyPair.getPrivate(), PASSWORD.toCharArray(),
+                new Certificate[] { certificate, ca });
+        return keyStore;
+    }
+
+    private static X509Certificate certificate(KeyPair keyPair, String commonName, String subjectAltName,
+            PrivateKey issuerKey, X509Certificate issuer) throws Exception {
+        final X500Principal subject = new X500Principal("CN=" + commonName);
+        final Instant now = Instant.now();
+        final JcaX509v3CertificateBuilder builder =
+                new JcaX509v3CertificateBuilder(issuer != null ? issuer.getSubjectX500Principal() : subject,
+                        BigInteger.valueOf(now.toEpochMilli()), Date.from(now.minus(Duration.ofDays(1))),
+                        Date.from(now.plus(Duration.ofDays(365))), subject, keyPair.getPublic());
+        builder.addExtension(Extension.basicConstraints, true, new BasicConstraints(issuer == null));
+        if (subjectAltName != null) {
+            builder.addExtension(Extension.subjectAlternativeName, false,
+                    new GeneralNames(new GeneralName(GeneralName.dNSName, subjectAltName)));
+        }
+        final ContentSigner signer = new JcaContentSignerBuilder(SIGNATURE_ALGORITHM).build(issuerKey);
+        return new JcaX509CertificateConverter().getCertificate(builder.build(signer));
+    }
+
+    private static KeyPair keyPair() throws Exception {
+        final KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
+        generator.initialize(2048);
+        return generator.generateKeyPair();
+    }
+
+    private static KeyStore emptyStore() throws Exception {
+        final KeyStore store = KeyStore.getInstance(KeyStore.getDefaultType());
+        store.load(null, null);
+        return store;
+    }
+}

-- 
To view, visit https://asterix-gerrit.ics.uci.edu/c/asterixdb/+/21999?usp=email
To unsubscribe, or for help writing mail filters, visit https://asterix-gerrit.ics.uci.edu/settings?usp=email

Gerrit-MessageType: newchange
Gerrit-Project: asterixdb
Gerrit-Branch: trinity
Gerrit-Change-Id: I9dd86f095f6a6c840281bda95e29fed4ee4cac91
Gerrit-Change-Number: 21999
Gerrit-PatchSet: 1
Gerrit-Owner: Michael Blow <mb...@apache.org>