You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@asterixdb.apache.org by mb...@apache.org on 2026/10/08 22:05:04 UTC
(asterixdb) 01/13: [ASTERIXDB-3851][NET][BP] Identify the peer on inter-node TLS
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/asterixdb.git
commit 9ebad090a457ab2258c67efa6457cbe26455ef7c
Author: Michael Blow <mi...@couchbase.com>
AuthorDate: Tue Sep 15 21:06:39 2026 -0400
[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
Reviewed-on: https://asterix-gerrit.ics.uci.edu/c/asterixdb/+/21999
Integration-Tests: Jenkins <je...@fulliautomatix.ics.uci.edu>
Reviewed-by: Michael Blow <mb...@apache.org>
Tested-by: Jenkins <je...@fulliautomatix.ics.uci.edu>
Reviewed-by: Ian Maxon <im...@apache.org>
Reviewed-by: Hussain Towaileb <hu...@gmail.com>
---
CLAUDE.md | 79 +++----
.../api/cluster_state_1/cluster_state_1.1.regexadm | 3 +
.../cluster_state_1_full.1.regexadm | 3 +
.../cluster_state_1_less.1.regexadm | 3 +
.../src/test/resources/security/cc/cc.crt | 44 ++--
.../src/test/resources/security/cc/cc.jks | Bin 2310 -> 2418 bytes
.../src/test/resources/security/cc/cc.p12 | Bin 2565 -> 2712 bytes
.../test/resources/security/nc1/asterix_nc1.crt | 44 ++--
.../test/resources/security/nc1/asterix_nc1.jks | Bin 2320 -> 2428 bytes
.../test/resources/security/nc1/asterix_nc1.p12 | Bin 2573 -> 2720 bytes
.../test/resources/security/nc2/asterix_nc2.crt | 44 ++--
.../test/resources/security/nc2/asterix_nc2.jks | Bin 2322 -> 2430 bytes
.../test/resources/security/nc2/asterix_nc2.p12 | Bin 2573 -> 2720 bytes
.../apache/asterix/metadata/RMIClientFactory.java | 30 ++-
.../api/network/INetworkSecurityConfig.java | 22 ++
.../api/network/INetworkSecurityManager.java | 18 +-
.../control/cc/ClusterControllerService.java | 5 +-
.../common/controllers/ControllerConfig.java | 36 +++-
.../hyracks/control/nc/NodeControllerService.java | 5 +-
hyracks-fullstack/hyracks/hyracks-ipc/pom.xml | 12 ++
.../ipc/security/NetworkSecurityConfig.java | 35 ++-
.../ipc/security/NetworkSecurityManager.java | 28 ++-
.../ipc/sockets/SslSocketChannelFactory.java | 29 ++-
.../hyracks/ipc/tests/SslPeerIdentityTest.java | 238 +++++++++++++++++++++
24 files changed, 537 insertions(+), 141 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index 93bd78016c..b604fda7e4 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -128,58 +128,27 @@ original:
git log -1 --format=%B | grep -c '^Change-Id:'
```
-## AI Provenance Annotation
-
-**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.
-
-- **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.
-
-### Attributes
-
-| 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 |
-
-### How to inject it
-
-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 { ... }
-```
+### Keep the body short
+
+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.
+
+## Code Comments
+
+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.
+
+Javadoc describing contract — parameters, return values, thrown exceptions, threading expectations
+— is not "what" commentary and is welcome.
+
+## AI Contributions
+
+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 7db9ee1dd6..251a2c63f9 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 1c6f3432e5..fd93d150d7 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 49251a9817..f7e2885fb2 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 94740b1542..3016d85818 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 7aca1874ff..6bcbdc5e98 100644
Binary files a/asterixdb/asterix-app/src/test/resources/security/cc/cc.jks and b/asterixdb/asterix-app/src/test/resources/security/cc/cc.jks 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 751bb8e1aa..656d2b9070 100644
Binary files a/asterixdb/asterix-app/src/test/resources/security/cc/cc.p12 and b/asterixdb/asterix-app/src/test/resources/security/cc/cc.p12 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 8fbff0ab92..b87128654d 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 ecbde7e788..a186509b9a 100644
Binary files a/asterixdb/asterix-app/src/test/resources/security/nc1/asterix_nc1.jks and b/asterixdb/asterix-app/src/test/resources/security/nc1/asterix_nc1.jks 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 0736eae82b..62553941b8 100644
Binary files a/asterixdb/asterix-app/src/test/resources/security/nc1/asterix_nc1.p12 and b/asterixdb/asterix-app/src/test/resources/security/nc1/asterix_nc1.p12 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 486d2a082f..c0eda8546e 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 df4f83f256..d5c43781e5 100644
Binary files a/asterixdb/asterix-app/src/test/resources/security/nc2/asterix_nc2.jks and b/asterixdb/asterix-app/src/test/resources/security/nc2/asterix_nc2.jks 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 4839db9f46..61fbcb4a75 100644
Binary files a/asterixdb/asterix-app/src/test/resources/security/nc2/asterix_nc2.p12 and b/asterixdb/asterix-app/src/test/resources/security/nc2/asterix_nc2.p12 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 ce459e27f7..55c393c276 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.net.Socket;
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 @@ public class RMIClientFactory implements RMIClientSocketFactory, Serializable {
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 7fc0335c5c..d71f188a5b 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
@@ -33,6 +33,28 @@ public interface INetworkSecurityConfig extends Serializable {
*/
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
*
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 eb52436d4e..90bd171c30 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 @@ public interface INetworkSecurityManager {
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 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 newSSLEngine();
+ 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 1dd54ad130..47bdc1caaf 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 @@ public class ClusterControllerService implements IControllerService {
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 19eba57d88..16d938a595 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 @@ public class ControllerConfig implements Serializable {
(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 @@ public class ControllerConfig implements Serializable {
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 @@ public class ControllerConfig implements Serializable {
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 90d4255b1a..59b2800f93 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 @@ public class NodeControllerService implements IControllerService {
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 35ebde2aed..00fd85f7b5 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 bfcd623900..ca9627103b 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 @@ public class NetworkSecurityConfig implements INetworkSecurityConfig {
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 class NetworkSecurityConfig implements INetworkSecurityConfig {
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
@@ -67,6 +84,16 @@ public class NetworkSecurityConfig implements INetworkSecurityConfig {
return sslEnabled;
}
+ @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 db524ca3d5..f6a35c4ce4 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 @@ public class NetworkSecurityManager implements INetworkSecurityManager {
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 @@ public class NetworkSecurityManager implements INetworkSecurityManager {
}
@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 2a2fb62830..593d87bac4 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 @@ public class SslSocketChannelFactory implements ISocketChannelFactory {
@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 0000000000..e4c480f3be
--- /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;
+ }
+}