Skip to content

Issue #8285 : Add Hadoop-free HDFS VFS plugin (WebHDFS/HttpFS/Knox) - #8287

Open
mattcasters wants to merge 12 commits into
apache:mainfrom
mattcasters:issue-8285
Open

Issue #8285 : Add Hadoop-free HDFS VFS plugin (WebHDFS/HttpFS/Knox)#8287
mattcasters wants to merge 12 commits into
apache:mainfrom
mattcasters:issue-8285

Conversation

@mattcasters

Copy link
Copy Markdown
Contributor

Adds a marketplace HDFS VFS plugin so Hop pipelines (including Parquet File Output) can write to HDFS without shipping Hadoop client JARs.

Parquet already writes through HopVfs.getOutputStream(). Matching Cloudera parcel libraries in Hop would fight the Hadoop 3.4.2 bits already on the classpath and would break on a 7.1.8 (Hadoop 3.1.1) → 7.3.2 (Hadoop 3.4.2) upgrade. This plugin talks the stable WebHDFS REST API instead.

What changed

  • hop-tech-hadoop marketplace plugin. Named VFS connection (scheme is the connection name, e.g. cdp:///warehouse/...). Does not register a global hdfs:// provider — Native Spark already uses that as a Hadoop FileSystem URI.
  • Transports: HttpFS (one gateway, recommended from OpenShift), Knox, WebHDFS (NameNode HTTP plus DataNode redirects).
  • Kerberos: JAAS keytab login, SPNEGO via the JDK GSS API, process-wide re-login at ~80% of TGT lifetime. No kinit binary, no UserGroupInformation.
  • Streaming writes. Piped HTTP PUT so Parquet is not buffered in heap.
  • Docs for the connection, VFS, and writing Parquet from OpenShift to CDP.

Tests

  • 20 unit tests against an in-process WebHDFS stub (mkdir, list, HttpFS create, WebHDFS noredirect create, streaming close, path encoding, named-connection scheme).
  • Docker IT project integration-tests/hadoop (unsecured WebHDFS + text/Parquet). Not run against a live cluster in this change.

OpenShift / CDP note

From a remote container, use HttpFS or Knox. Raw WebHDFS and native HDFS RPC both need DataNode ports from the pod.

Fixes #8285

…nox)

Named HDFS connections talk REST (HttpFS, Knox, or WebHDFS) with JDK
Kerberos/SPNEGO and keytab re-login. No Hadoop client JARs, so Parquet
and other VFS writers can reach CDP 7.1.8 and 7.3.2 from OpenShift
without matching cluster libraries. Marketplace-only; does not register
a global hdfs:// scheme.
Keep both how-to nav entries: HDFS/OpenShift from this branch and
scheduling from apache#8273.
…ection

Each Cluster, Kerberos and TLS tab gets a Test button that probes the
values currently in the editor. TLS trust material accepts PEM/CRT CA
bundles as well as JKS/PKCS12 keystores.
…ugin classloaders

GuiPluginType ignored classLoaderGroup, so the editor buttons ran against a
second copy of HdfsMeta. Share vfs-hdfs with the GUI plugin and invoke the
button method on the live editor object.
@mattcasters

Copy link
Copy Markdown
Contributor Author

With TLS and Kerberos working I got the following error testing the WebHDFS endpoint:

IOException: SPNEGO token failed for https://master1.bda2.vdab.be:9871/webhdfs/v1/?op=GETFILESTATUS
Root cause: GSSException: No valid credentials provided (Mechanism level: Failed to find any Kerberos tgt)

Detailed stack trace:

java.io.IOException: SPNEGO token failed for https://master1.bda2.vdab.be:9871/webhdfs/v1/?op=GETFILESTATUS
	at org.apache.hop.vfs.hdfs.client.HdfsWebHdfsClient.addSpnego(HdfsWebHdfsClient.java:318)
	at org.apache.hop.vfs.hdfs.client.HdfsWebHdfsClient.request(HdfsWebHdfsClient.java:307)
	at org.apache.hop.vfs.hdfs.client.HdfsWebHdfsClient.executeString(HdfsWebHdfsClient.java:241)
	at org.apache.hop.vfs.hdfs.client.HdfsWebHdfsClient.getFileStatus(HdfsWebHdfsClient.java:103)
	at org.apache.hop.vfs.hdfs.HdfsConnectionTester.testCluster(HdfsConnectionTester.java:59)
	at org.apache.hop.vfs.hdfs.metadata.HdfsMeta.runTest(HdfsMeta.java:391)
	at org.apache.hop.vfs.hdfs.metadata.HdfsMeta.testClusterButton(HdfsMeta.java:191)
	at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
	at java.base/java.lang.reflect.Method.invoke(Method.java:580)
	at org.apache.hop.core.gui.plugin.GuiWidgetMethodInvoker.invoke(GuiWidgetMethodInvoker.java:44)
	at org.apache.hop.ui.core.gui.GuiCompositeWidgets.lambda$getButtonControl$6(GuiCompositeWidgets.java:795)
	at org.eclipse.swt.widgets.EventTable.sendEvent(EventTable.java:91)
	at org.eclipse.swt.widgets.Display.sendEvent(Display.java:4363)
	at org.eclipse.swt.widgets.Widget.sendEvent(Widget.java:1217)
	at org.eclipse.swt.widgets.Display.runDeferredEvents(Display.java:4161)
	at org.eclipse.swt.widgets.Display.readAndDispatch(Display.java:3749)
	at org.apache.hop.ui.hopgui.HopGui.open(HopGui.java:820)
	at org.apache.hop.ui.hopgui.HopGui.main(HopGui.java:649)
Caused by: GSSException: No valid credentials provided (Mechanism level: No valid credentials provided (Mechanism level: Failed to find any Kerberos tgt))
	at java.security.jgss/sun.security.jgss.spnego.SpNegoContext.initSecContext(SpNegoContext.java:457)
	at java.security.jgss/sun.security.jgss.GSSContextImpl.initSecContext(GSSContextImpl.java:266)
	at java.security.jgss/sun.security.jgss.GSSContextImpl.initSecContext(GSSContextImpl.java:196)
	at org.apache.hop.vfs.hdfs.client.HdfsSpnego.authorizationHeader(HdfsSpnego.java:44)
	at org.apache.hop.vfs.hdfs.client.HdfsWebHdfsClient.addSpnego(HdfsWebHdfsClient.java:316)
	... 17 more
Caused by: GSSException: No valid credentials provided (Mechanism level: Failed to find any Kerberos tgt)
	at java.security.jgss/sun.security.jgss.krb5.Krb5InitCredential.getInstance(Krb5InitCredential.java:164)
	at java.security.jgss/sun.security.jgss.krb5.Krb5MechFactory.getCredentialElement(Krb5MechFactory.java:126)
	at java.security.jgss/sun.security.jgss.krb5.Krb5MechFactory.getMechanismContext(Krb5MechFactory.java:194)
	at java.security.jgss/sun.security.jgss.GSSManagerImpl.getMechanismContext(GSSManagerImpl.java:206)
	at java.security.jgss/sun.security.jgss.GSSContextImpl.initSecContext(GSSContextImpl.java:230)
	at java.security.jgss/sun.security.jgss.GSSContextImpl.initSecContext(GSSContextImpl.java:196)
	at java.security.jgss/sun.security.jgss.spnego.SpNegoContext.GSS_initSecContext(SpNegoContext.java:882)
	at java.security.jgss/sun.security.jgss.spnego.SpNegoContext.initSecContext(SpNegoContext.java:320)
	... 21 more

``

… to forward slashes

JAAS treats backslash as an escape, so a Browse result like C:\Users\...
breaks Kerberos login. Store and use C:/Users/... instead.
…er test steps

The Cluster Test button logged in, then built the Negotiate token on the
calling thread. With useSubjectCredsOnly the GSS layer never saw the TGT
(Failed to find any Kerberos tgt). Token creation now runs in doAs, and
the Test dialog lists TLS, Kerberos, SPNEGO and GETFILESTATUS so a later
failure names the step that actually broke.
… NameNode

GETFILESTATUS 403 "state standby" means the WebHDFS host is the HA standby.
Endpoint hostname now takes Impala-style comma-separated hosts
(master1,master2) and retries the next one. Test dialogs use the title
"Test results" so the window no longer stretches off-screen.
@mattcasters
mattcasters marked this pull request as ready for review September 9, 2026 10:44
…ve NameNode

OPEN now uses noredirect and fetches the DataNode Location, matching CREATE, so an empty 307 is not treated as the file. The HTTP client disables gzip and automatic redirects. After HA failover the working NameNode is tried first, and standby messages are logDetailed.
@mattcasters
mattcasters marked this pull request as draft September 9, 2026 12:53
@mattcasters

Copy link
Copy Markdown
Contributor Author

Found an issue when writing to HDFS:

2026/09/09 14:52:34 [Error]    acde://vdp/vdp-test/matt/csv/random-data*.csv.0 - Couldn't open file acde:///vdp/vdp-test/matt/csv/random-data.csv
2026/09/09 14:52:34 [Error]    acde://vdp/vdp-test/matt/csv/random-data*.csv.0 - org.apache.hop.core.exception.HopFileException: 
2026/09/09 14:52:34 [Error]    acde://vdp/vdp-test/matt/csv/random-data*.csv.0 - 
2026/09/09 14:52:34 [Error]    acde://vdp/vdp-test/matt/csv/random-data*.csv.0 - org.apache.commons.vfs2.FileSystemException: Could not determine the type of file "acde:///vdp/vdp-test/matt/csv/random-data_0.csv".
2026/09/09 14:52:34 [Error]    acde://vdp/vdp-test/matt/csv/random-data*.csv.0 - Could not determine the type of file "acde:///vdp/vdp-test/matt/csv/random-data_0.csv".
2026/09/09 14:52:34 [Error]    acde://vdp/vdp-test/matt/csv/random-data*.csv.0 - 
2026/09/09 14:52:34 [Error]    acde://vdp/vdp-test/matt/csv/random-data*.csv.0 - Could not determine the type of file "acde:///vdp/vdp-test/matt/csv/random-data_0.csv".
2026/09/09 14:52:34 [Error]    acde://vdp/vdp-test/matt/csv/random-data*.csv.0 - 
2026/09/09 14:52:34 [Error]    acde://vdp/vdp-test/matt/csv/random-data*.csv.0 - 	at org.apache.hop.core.vfs.HopVfs.getOutputStream(HopVfs.java:646)
2026/09/09 14:52:34 [Error]    acde://vdp/vdp-test/matt/csv/random-data*.csv.0 - 	at org.apache.hop.core.vfs.HopVfs.getOutputStream(HopVfs.java:637)
2026/09/09 14:52:34 [Error]    acde://vdp/vdp-test/matt/csv/random-data*.csv.0 - 	at org.apache.hop.pipeline.transforms.textfileoutput.TextFileOutput.getOutputStream(TextFileOutput.java:1268)
2026/09/09 14:52:34 [Error]    acde://vdp/vdp-test/matt/csv/random-data*.csv.0 - 	at org.apache.hop.pipeline.transforms.textfileoutput.TextFileOutput.initFileStreamWriter(TextFileOutput.java:187)
2026/09/09 14:52:34 [Error]    acde://vdp/vdp-test/matt/csv/random-data*.csv.0 - 	at org.apache.hop.pipeline.transforms.textfileoutput.TextFileOutput.initOutput(TextFileOutput.java:1053)
2026/09/09 14:52:34 [Error]    acde://vdp/vdp-test/matt/csv/random-data*.csv.0 - 	at org.apache.hop.pipeline.transforms.textfileoutput.TextFileOutput.init(TextFileOutput.java:1023)
2026/09/09 14:52:34 [Error]    acde://vdp/vdp-test/matt/csv/random-data*.csv.0 - 	at org.apache.hop.pipeline.transform.TransformInitThread.run(TransformInitThread.java:78)
2026/09/09 14:52:34 [Error]    acde://vdp/vdp-test/matt/csv/random-data*.csv.0 - 	at java.base/java.lang.Thread.run(Thread.java:1583)
2026/09/09 14:52:34 [Error]    acde://vdp/vdp-test/matt/csv/random-data*.csv.0 - Caused by: org.apache.commons.vfs2.FileSystemException: Could not determine the type of file "acde:///vdp/vdp-test/matt/csv/random-data_0.csv".
2026/09/09 14:52:34 [Error]    acde://vdp/vdp-test/matt/csv/random-data*.csv.0 - 	at org.apache.commons.vfs2.provider.AbstractFileObject.attach(AbstractFileObject.java:173)
2026/09/09 14:52:34 [Error]    acde://vdp/vdp-test/matt/csv/random-data*.csv.0 - 	at org.apache.commons.vfs2.provider.AbstractFileObject.getType(AbstractFileObject.java:1376)
2026/09/09 14:52:34 [Error]    acde://vdp/vdp-test/matt/csv/random-data*.csv.0 - 	at org.apache.commons.vfs2.provider.AbstractFileObject.exists(AbstractFileObject.java:968)
2026/09/09 14:52:34 [Error]    acde://vdp/vdp-test/matt/csv/random-data*.csv.0 - 	at org.apache.hop.core.vfs.HopVfs.getOutputStream(HopVfs.java:605)
2026/09/09 14:52:34 [Error]    acde://vdp/vdp-test/matt/csv/random-data*.csv.0 - 	at org.apache.hop.core.vfs.HopVfs.getOutputStream(HopVfs.java:644)
2026/09/09 14:52:34 [Error]    acde://vdp/vdp-test/matt/csv/random-data*.csv.0 - 	... 7 more
2026/09/09 14:52:34 [Error]    acde://vdp/vdp-test/matt/csv/random-data*.csv.0 - Caused by: java.io.IOException: GET /webhdfs/v1/vdp/vdp-test/matt/csv/random-data_0.csv?op=GETFILESTATUS failed with HTTP 403: Operation category READ is not supported in state standby. Visit https://s.apache.org/sbnn-error
2026/09/09 14:52:34 [Error]    acde://vdp/vdp-test/matt/csv/random-data*.csv.0 - 	at org.apache.hop.vfs.hdfs.client.HdfsWebHdfsClient.lambda$executeRequest$4(HdfsWebHdfsClient.java:394)
2026/09/09 14:52:34 [Error]    acde://vdp/vdp-test/matt/csv/random-data*.csv.0 - 	at org.apache.hc.client5.http.impl.classic.CloseableHttpClient.execute(CloseableHttpClient.java:247)
2026/09/09 14:52:34 [Error]    acde://vdp/vdp-test/matt/csv/random-data*.csv.0 - 	at org.apache.hc.client5.http.impl.classic.CloseableHttpClient.execute(CloseableHttpClient.java:188)
2026/09/09 14:52:34 [Error]    acde://vdp/vdp-test/matt/csv/random-data*.csv.0 - 	at org.apache.hc.client5.http.impl.classic.CloseableHttpClient.execute(CloseableHttpClient.java:162)
2026/09/09 14:52:34 [Error]    acde://vdp/vdp-test/matt/csv/random-data*.csv.0 - 	at org.apache.hop.vfs.hdfs.client.HdfsWebHdfsClient.lambda$executeRequest$5(HdfsWebHdfsClient.java:387)
2026/09/09 14:52:34 [Error]    acde://vdp/vdp-test/matt/csv/random-data*.csv.0 - 	at java.base/java.security.AccessController.doPrivileged(AccessController.java:714)
2026/09/09 14:52:34 [Error]    acde://vdp/vdp-test/matt/csv/random-data*.csv.0 - 	at java.base/javax.security.auth.Subject.doAs(Subject.java:525)
2026/09/09 14:52:34 [Error]    acde://vdp/vdp-test/matt/csv/random-data*.csv.0 - 	at org.apache.hop.vfs.hdfs.kerberos.HdfsKerberosSession.doAs(HdfsKerberosSession.java:111)
2026/09/09 14:52:34 [Error]    acde://vdp/vdp-test/matt/csv/random-data*.csv.0 - 	at org.apache.hop.vfs.hdfs.client.HdfsWebHdfsClient.privileged(HdfsWebHdfsClient.java:407)
2026/09/09 14:52:34 [Error]    acde://vdp/vdp-test/matt/csv/random-data*.csv.0 - 	at org.apache.hop.vfs.hdfs.client.HdfsWebHdfsClient.executeRequest(HdfsWebHdfsClient.java:385)
2026/09/09 14:52:34 [Error]    acde://vdp/vdp-test/matt/csv/random-data*.csv.0 - 	at org.apache.hop.vfs.hdfs.client.HdfsWebHdfsClient.executeString(HdfsWebHdfsClient.java:360)
2026/09/09 14:52:34 [Error]    acde://vdp/vdp-test/matt/csv/random-data*.csv.0 - 	at org.apache.hop.vfs.hdfs.client.HdfsWebHdfsClient.getFileStatus(HdfsWebHdfsClient.java:107)
2026/09/09 14:52:34 [Error]    acde://vdp/vdp-test/matt/csv/random-data*.csv.0 - 	at org.apache.hop.vfs.hdfs.HdfsFileObject.doAttach(HdfsFileObject.java:61)
2026/09/09 14:52:34 [Error]    acde://vdp/vdp-test/matt/csv/random-data*.csv.0 - 	at org.apache.commons.vfs2.provider.AbstractFileObject.attach(AbstractFileObject.java:164)
2026/09/09 14:52:34 [Error]    acde://vdp/vdp-test/matt/csv/random-data*.csv.0 - 	... 11 more

@mattcasters

Copy link
Copy Markdown
Contributor Author

And a StackOverflowException:

2026/09/09 14:54:24 [Error]    acde://vdp/vdp-test/matt/csv/random-data*.csv.0 - Error initializing transform [acde://vdp/vdp-test/matt/csv/random-data*.csv]
2026/09/09 14:54:24 [Error]    acde://vdp/vdp-test/matt/csv/random-data*.csv.0 - java.lang.StackOverflowError
2026/09/09 14:54:24 [Error]    acde://vdp/vdp-test/matt/csv/random-data*.csv.0 - 	at org.apache.commons.vfs2.provider.AbstractFileObject.getType(AbstractFileObject.java:1381)
2026/09/09 14:54:24 [Error]    acde://vdp/vdp-test/matt/csv/random-data*.csv.0 - 	at org.apache.hop.vfs.hdfs.HdfsFileObject.doGetType(HdfsFileObject.java:92)
2026/09/09 14:54:24 [Error]    acde://vdp/vdp-test/matt/csv/random-data*.csv.0 - 	at org.apache.commons.vfs2.provider.AbstractFileObject.getType(AbstractFileObject.java:1381)
2026/09/09 14:54:24 [Error]    acde://vdp/vdp-test/matt/csv/random-data*.csv.0 - 	at org.apache.hop.vfs.hdfs.HdfsFileObject.doGetType(HdfsFileObject.java:92)
2026/09/09 14:54:24 [Error]    acde://vdp/vdp-test/matt/csv/random-data*.csv.0 - 	at org.apache.commons.vfs2.provider.AbstractFileObject.getType(AbstractFileObject.java:1381)
2026/09/09 14:54:24 [Error]    acde://vdp/vdp-test/matt/csv/random-data*.csv.0 - 	at org.apache.hop.vfs.hdfs.HdfsFileObject.doGetType(HdfsFileObject.java:92)
2026/09/09 14:54:24 [Error]    acde://vdp/vdp-test/matt/csv/random-data*.csv.0 - 	at org.apache.commons.vfs2.provider.AbstractFileObject.getType(AbstractFileObject.java:1381)
2026/09/09 14:54:24 [Error]    acde://vdp/vdp-test/matt/csv/random-data*.csv.0 - 	at org.apache.hop.vfs.hdfs.HdfsFileObject.doGetType(HdfsFileObject.java:92)
...

GZIPInputStream throws EOFException when an HTTP/VFS body ends a few bytes short of the CRC trailer. Map that to -1 so Text File Input can finish a large gzip CSV, and buffer the inflater and HDFS entity reads.
doGetType no longer calls getType (StackOverflowError with multiple Text File Output copies). doAttach no longer marks the file attached before GETFILESTATUS. A 404 from the active NameNode is remembered so CREATE does not go back to the standby. Only standby and unreachable hosts fail over.
@mattcasters
mattcasters marked this pull request as ready for review September 9, 2026 13:24
@mattcasters
mattcasters marked this pull request as draft September 9, 2026 13:31
@mattcasters

Copy link
Copy Markdown
Contributor Author

Looks like CRUD is working. The parallel write/read/... edge cases are being put into extra integration tests.

… in ITs

Extend integration-tests/hadoop with gzip CSV write/read, four-copy Text File Output, parquet read-back, and a WebHDFS connection whose first NameNode is unreachable so write/read must fail over.
@mattcasters
mattcasters marked this pull request as ready for review September 9, 2026 13:47
@mattcasters

Copy link
Copy Markdown
Contributor Author

Security & Integrity Review for PR #8287 / Issue #8285

Overall, moving to a Hadoop-free WebHDFS / HttpFS / Knox VFS provider is a huge security win—it eliminates hundreds of transitive dependencies, outdated RPC libraries, and common CVEs. Hostname verification is enabled by default, SPNEGO credential delegation is disabled (requestCredDeleg(false)), and paths are normalized by VFS.

Here is a summary of security, data integrity, and credential-handling findings to address:


1. [Critical / Data Integrity] Silent Data Truncation in GzipCompressionInputStream

  • Location: engine/src/main/java/org/apache/hop/core/compress/gzip/GzipCompressionInputStream.java:54-72
  • Issue: In read() and read(byte[], int, int), catching EOFException and returning -1 masks truncated gzip streams:
    catch (EOFException e) {
      // Network/VFS streams often end with -1 while the inflater still wants the gzip trailer.
      return -1;
    }
  • Impact: GzipCompressionInputStream is used globally by all transforms reading .gz files (CSV, Text File Input, Parquet, JSON, etc.). If a download is aborted or a file is truncated, java.util.zip.GZIPInputStream throws EOFException because the DEFLATE stream or 8-byte trailer (CRC32 + ISIZE) is missing. Converting this to -1 causes pipelines to complete with SUCCESS on partial data, silently corrupting data in downstream databases/warehouses and completely skipping CRC32 validation.
  • Recommendation: The 64KB buffering added in HdfsWebHdfsClient.streamEntity and GzipCompressionInputStream already resolves the issue of reading trailers across packet boundaries. Swallowing EOFException globally should be removed so truncated files fail fast.

2. [High] Unconditional SPNEGO Ticket Acquisition in HdfsWebHdfsClient.putStream

  • Location: plugins/tech/hadoop/src/main/java/org/apache/hop/vfs/hdfs/client/HdfsWebHdfsClient.java:236-242
  • Issue: While getAbsoluteStream guards SPNEGO via if (shouldSpnegoForLocation(location)), putStream calls addSpnego(put, uri) unconditionally:
    private void putStream(String uri, InputStream body) throws IOException {
      HttpPut put = new HttpPut(uri);
      ...
      addSpnego(put, uri);
      executeRequest(put);
    }
  • Impact: During WebHDFS CREATE, NameNode returns a DataNode Location URL. addSpnego extracts the host and requests a Kerberos service ticket for HTTP@<datanode-host>, attaching Authorization: Negotiate <token> to the PUT request.
    1. If redirected to an untrusted host or external location, the user's SPNEGO Kerberos ticket is leaked.
    2. On standard Kerberized clusters, DataNodes expect delegation tokens in the query parameters (as noted in line 288); sending a SPNEGO ticket to DataNodes can fail if no SPN exists for individual DataNodes or if the DataNode rejects it.
  • Fix: Guard with shouldSpnegoForLocation(uri) in putStream:
    if (shouldSpnegoForLocation(uri)) {
      addSpnego(put, uri);
    }

3. [Medium] Missing Password Decryption for truststorePassword in HdfsTls

  • Location: plugins/tech/hadoop/src/main/java/org/apache/hop/vfs/hdfs/HdfsTls.java:66
  • Issue: HdfsMeta.truststorePassword is annotated with @HopMetadataProperty(password = true). Hop encrypts these values upon saving (e.g. Encrypted 2be98af...). In HdfsTls.loadKeyStore, the raw value is passed to keyStore.load() without decrypting:
    loadKeyStore(bytes, variables.resolve(Const.NVL(meta.getTruststorePassword(), "")));
  • Impact: Saved connections with password-protected keystores (JKS/PKCS12) will fail to open on reload because the ciphertext is used as the password.
  • Fix: Wrap with Encr.decryptPasswordOptionallyEncrypted(...):
    String password = Encr.decryptPasswordOptionallyEncrypted(
        variables.resolve(Const.NVL(meta.getTruststorePassword(), "")));
    return loadKeyStore(bytes, password);

4. [Medium] Potential Protocol Downgrade on Redirects (HTTPS -> HTTP)

  • Location: plugins/tech/hadoop/src/main/java/org/apache/hop/vfs/hdfs/client/HdfsWebHdfsClient.java:160-167, 209-214
  • Issue: When the connection is configured for HTTPS (meta.isHttps() or Knox), the initial request is encrypted. However, WebHDFS 307 redirects or JSON Location values from NameNode contain absolute URLs.
  • Impact: If a cluster returns an http:// DataNode URL (misconfiguration or proxy/MITM), getAbsoluteStream and putStream will silently connect over plaintext HTTP, transmitting data and any delegation tokens unencrypted.
  • Recommendation: If httpScheme is "https", enforce that location starts with https:// (or rewrite/reject if downgraded to http://).

5. [Medium / Architectural] JVM-Wide System Property Mutation in HdfsKerberosSession

  • Location: plugins/tech/hadoop/src/main/java/org/apache/hop/vfs/hdfs/kerberos/HdfsKerberosSession.java:164-178
  • Issue: applyJvmKerberosConfig sets java.security.krb5.conf, realm, and kdc via System.setProperty().
  • Impact: These properties are JVM-wide. In Hop Server or Hop GUI environments where multiple HDFS connections or other Kerberized services (Kafka, JDBC) run concurrently with different realms, modifying global system properties can cause cross-connection collisions.

6. [Low] Monotonic Session Growth in HdfsKerberosRenewer

  • Location: plugins/tech/hadoop/src/main/java/org/apache/hop/vfs/hdfs/kerberos/HdfsKerberosRenewer.java:43-48
  • Issue: Every connection test or VFS filesystem creation instantiates a new HdfsKerberosSession and registers it in HdfsKerberosRenewer. Because sessions are never unregistered and don't implement equals/hashCode, sessions grows monotonically.
  • Impact: A long-running Hop instance (like Hop Server or GUI) will keep attempting re-login every minute for dead/test sessions, keeping credentials and Subject references in memory.
  • Recommendation: Add an unregister(HdfsKerberosSession) method and call it when the VFS filesystem is closed or a test probe finishes.

@mattcasters
mattcasters marked this pull request as draft September 10, 2026 08:21
…beros)

Stop treating truncated gzip as success. Mint SPNEGO only for configured
gateway hosts. Decrypt truststore passwords. Reject HTTP DataNode
Locations on HTTPS connections (opt-in checkbox). Serialize JVM Kerberos
config, ignore Realm/KDC when krb5.conf is set, and unregister sessions
when tests and filesystems close.
@mattcasters

Copy link
Copy Markdown
Contributor Author

Addressed the security review:

  1. Gzip — no longer swallows EOFException; truncated files fail. 64KB buffering stays. Chunked complete gzip still reads.
  2. SPNEGOputStream uses shouldSpnegoForLocation (NameNode/HttpFS/Knox only, not DataNodes).
  3. Truststore passwordEncr.decryptPasswordOptionallyEncrypted before KeyStore.load.
  4. HTTPS→HTTP — DataNode Location: http:// is rejected when the connection is HTTPS. TLS tab: Allow HTTP DataNode redirects (default off).
  5. Kerberos JVM properties — login/SPNEGO under a process lock; krb5.conf wins over Realm/KDC fields.
  6. Renewerunregister on Test-button finally and VFS filesystem close (HTTP client closed too).

Rebuild engine + Hadoop plugin before retesting gzip reads and WebHDFS CREATE.

@mattcasters

Copy link
Copy Markdown
Contributor Author

Follow-up Review: Security & Integrity Recommendations Addressed in bdcae03

Commit bdcae03ada cleanly addresses all 6 items identified in the review:

  1. GZIP Stream Integrity (GzipCompressionInputStream): Removed the EOFException catch block that masked truncated streams. Truncated gzip files now fail fast and preserve CRC32 validation (truncatedGzipFailsInsteadOfSilentSuccess).
  2. SPNEGO Protection on Writes (HdfsWebHdfsClient): putStream now guards addSpnego with shouldSpnegoForLocation(uri). Kerberos service tickets are no longer minted or sent to DataNode URLs.
  3. Keystore Password Decryption (HdfsTls): Wrapped meta.getTruststorePassword() with Encr.decryptPasswordOptionallyEncrypted(...), ensuring saved/encrypted truststore passwords load correctly.
  4. HTTPS Downgrade Protection: Added rejectHttpDowngrade() to prevent silent redirects to unencrypted HTTP DataNodes on HTTPS connections, along with an opt-in allowHttpDatanodeRedirects setting (default: off) for legacy clusters.
  5. JVM Kerberos Serialization (HdfsKerberosSession): Synchronized Kerberos configuration and login around JVM_KERBEROS to prevent concurrency races in multi-connection/server environments; avoided clobbering Realm/KDC when krb5.conf is provided.
  6. Session & HTTP Client Lifecycle: Added unregister() to HdfsKerberosRenewer and lifecycle cleanup in session.close(), HdfsConnectionTester, and HdfsFileSystem.doCloseCommunicationLink(), closing both kerberosSession and httpClient to prevent leaks.

All unit tests pass and Spotless formatting is clean.

@mattcasters
mattcasters marked this pull request as ready for review September 10, 2026 08:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature Request]: Properly support the Apache Hadoop File System

1 participant