This is an automated email from the ASF dual-hosted git repository.

Duansg pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hertzbeat.git


The following commit(s) were added to refs/heads/master by this push:
     new eb753be1f5 maintenance: extend SFTP connection options (#4293)
eb753be1f5 is described below

commit eb753be1f5796997198137051d240c1a98347141
Author: Logic <[email protected]>
AuthorDate: Thu Aug 27 00:42:23 2026 +0800

    maintenance: extend SFTP connection options (#4293)
    
    Co-authored-by: Duansg <[email protected]>
---
 .../collector/collect/ftp/FtpCollectImpl.java      |  58 ++++++++++-
 .../collector/collect/ftp/FtpCollectImplTest.java  | 104 +++++++++++++++++++
 .../common/entity/job/protocol/FtpProtocol.java    |  84 +++++++++++++--
 .../entity/job/protocol/FtpProtocolTest.java       | 113 +++++++++++++++++++++
 .../src/main/resources/define/app-ftp.yml          |  26 +++++
 home/docs/help/ftp.md                              |  26 +++++
 home/docs/start/upgrade.md                         |  18 ++++
 .../current/help/ftp.md                            |  22 ++++
 .../current/start/upgrade.md                       |  15 +++
 .../monitor-form/monitor-form.component.spec.ts    |  50 +++++++++
 .../monitor/monitor-form/monitor-form.component.ts |  12 ++-
 11 files changed, 512 insertions(+), 16 deletions(-)

diff --git 
a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ftp/FtpCollectImpl.java
 
b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ftp/FtpCollectImpl.java
index 03307e1c7a..d7cfa6bb95 100644
--- 
a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ftp/FtpCollectImpl.java
+++ 
b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ftp/FtpCollectImpl.java
@@ -19,8 +19,11 @@ package org.apache.hertzbeat.collector.collect.ftp;
 
 import java.io.IOException;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
 
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.net.ftp.FTPClient;
@@ -32,7 +35,11 @@ import 
org.apache.hertzbeat.common.entity.job.protocol.FtpProtocol;
 import org.apache.hertzbeat.common.entity.message.CollectRep;
 import org.apache.hertzbeat.common.util.CommonUtil;
 import org.apache.sshd.client.SshClient;
+import org.apache.sshd.client.keyverifier.AcceptAllServerKeyVerifier;
+import org.apache.sshd.client.keyverifier.ServerKeyVerifier;
 import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.common.config.keys.KeyUtils;
+import org.apache.sshd.common.digest.BuiltinDigests;
 import org.apache.sshd.sftp.client.SftpClient;
 import org.apache.sshd.sftp.client.SftpClientFactory;
 import org.springframework.util.Assert;
@@ -46,6 +53,8 @@ public class FtpCollectImpl extends AbstractCollect {
 
     private static final String ANONYMOUS = "anonymous";
     private static final String PASSWORD = "password";
+    private static final int MAX_INSECURE_WARNING_ENDPOINTS = 1024;
+    private static final Set<String> INSECURE_WARNING_ENDPOINTS = 
ConcurrentHashMap.newKeySet();
 
     /**
      * preCheck params
@@ -56,10 +65,8 @@ public class FtpCollectImpl extends AbstractCollect {
             throw new IllegalArgumentException("Ftp collect must has ftp 
params.");
         }
         FtpProtocol ftpProtocol = metrics.getFtp();
-        Assert.hasText(ftpProtocol.getHost(), "Ftp Protocol host is 
required.");
-        Assert.hasText(ftpProtocol.getPort(), "Ftp Protocol port is 
required.");
-        Assert.hasText(ftpProtocol.getDirection(), "Ftp Protocol direction is 
required.");
-        Assert.hasText(ftpProtocol.getTimeout(), "Ftp Protocol timeout is 
required.");
+        String validationError = ftpProtocol.validationError();
+        Assert.isNull(validationError, validationError);
     }
 
     @Override
@@ -202,6 +209,7 @@ public class FtpCollectImpl extends AbstractCollect {
         SshClient client = null;
         try {
             client = SshClient.setUpDefaultClient();
+            client.setServerKeyVerifier(createServerKeyVerifier(ftpProtocol));
             session = connect(client, ftpProtocol);
             sftpClient = 
SftpClientFactory.instance().createSftpClient(session);
             Map<String, String> valueMap = collectValue(sftpClient, 
ftpProtocol);
@@ -229,4 +237,44 @@ public class FtpCollectImpl extends AbstractCollect {
             }
         }
     }
-}
\ No newline at end of file
+
+    static ServerKeyVerifier createServerKeyVerifier(FtpProtocol ftpProtocol) {
+        if (Boolean.parseBoolean(ftpProtocol.getInsecureSkipVerify())) {
+            logInsecureVerification(ftpProtocol);
+            return AcceptAllServerKeyVerifier.INSTANCE;
+        }
+        Assert.hasText(ftpProtocol.getHostKeyFingerprint(),
+                "Sftp Protocol host key fingerprint is required. "
+                        + "Obtain it through a trusted channel; see the FTP 
monitor guide.");
+        Assert.isTrue(ftpProtocol.hasValidHostKeyFingerprints(),
+                "Sftp Protocol host key fingerprints must use the 
SHA256:base64 format.");
+        List<String> expectedFingerprints = 
ftpProtocol.parseHostKeyFingerprints();
+        Assert.notEmpty(expectedFingerprints,
+                "Sftp Protocol host key fingerprint list must not be empty.");
+        return (clientSession, remoteAddress, serverKey) -> {
+            boolean matches = serverKey != null && 
expectedFingerprints.stream()
+                    .anyMatch(expectedFingerprint -> Boolean.TRUE.equals(
+                            KeyUtils.checkFingerPrint(
+                                    expectedFingerprint,
+                                    BuiltinDigests.sha256,
+                                    serverKey).getKey()));
+            if (!matches) {
+                log.warn("[SFTPClient] server host key did not match for 
{}:{}",
+                        ftpProtocol.getHost(), ftpProtocol.getPort());
+            }
+            return matches;
+        };
+    }
+
+    private static void logInsecureVerification(FtpProtocol ftpProtocol) {
+        String endpoint = Objects.toString(ftpProtocol.getHost(), "<unknown>")
+                + ':' + Objects.toString(ftpProtocol.getPort(), "<unknown>");
+        if (INSECURE_WARNING_ENDPOINTS.size() < MAX_INSECURE_WARNING_ENDPOINTS
+                && INSECURE_WARNING_ENDPOINTS.add(endpoint)) {
+            log.warn("[SFTPClient] host key verification is disabled for {}; "
+                    + "configure trusted host key fingerprints and re-enable 
verification", endpoint);
+        } else {
+            log.debug("[SFTPClient] host key verification remains disabled for 
{}", endpoint);
+        }
+    }
+}
diff --git 
a/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/ftp/FtpCollectImplTest.java
 
b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/ftp/FtpCollectImplTest.java
index 5af290d60d..916a441b40 100644
--- 
a/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/ftp/FtpCollectImplTest.java
+++ 
b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/ftp/FtpCollectImplTest.java
@@ -18,15 +18,25 @@
 package org.apache.hertzbeat.collector.collect.ftp;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
 import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.security.KeyPairGenerator;
 import java.util.ArrayList;
 import java.util.List;
 import org.apache.commons.net.ftp.FTPClient;
 import org.apache.hertzbeat.common.entity.job.Metrics;
 import org.apache.hertzbeat.common.entity.job.protocol.FtpProtocol;
 import org.apache.hertzbeat.common.entity.message.CollectRep;
+import org.apache.sshd.client.keyverifier.AcceptAllServerKeyVerifier;
+import org.apache.sshd.client.keyverifier.ServerKeyVerifier;
+import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.common.config.keys.KeyUtils;
+import org.apache.sshd.common.digest.BuiltinDigests;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.InjectMocks;
@@ -143,5 +153,99 @@ class FtpCollectImplTest {
 
     }
 
+    @Test
+    void serverKeyVerifierSupportsHostKeyRotationWindow() throws Exception {
+        var currentKey = generateEcPublicKey();
+        var nextKey = generateEcPublicKey();
+        var unrelatedKey = generateEcPublicKey();
+        FtpProtocol ftpProtocol = FtpProtocol.builder()
+                .host("sftp.example.com")
+                .port("22")
+                
.hostKeyFingerprint(KeyUtils.getFingerPrint(BuiltinDigests.sha256, currentKey)
+                        + System.lineSeparator()
+                        + KeyUtils.getFingerPrint(BuiltinDigests.sha256, 
nextKey))
+                .build();
+
+        ServerKeyVerifier verifier = 
FtpCollectImpl.createServerKeyVerifier(ftpProtocol);
+        ClientSession session = Mockito.mock(ClientSession.class);
+        InetSocketAddress address = 
InetSocketAddress.createUnresolved("sftp.example.com", 22);
+
+        assertTrue(verifier.verifyServerKey(session, address, currentKey));
+        assertTrue(verifier.verifyServerKey(session, address, nextKey));
+        assertFalse(verifier.verifyServerKey(session, address, unrelatedKey));
+    }
+
+    @Test
+    void serverKeyVerifierRejectsNonSha256Fingerprints() {
+        var serverKey = generateEcPublicKey();
+        FtpProtocol ftpProtocol = FtpProtocol.builder()
+                
.hostKeyFingerprint(KeyUtils.getFingerPrint(BuiltinDigests.md5, serverKey))
+                .build();
+
+        assertThrows(IllegalArgumentException.class,
+                () -> FtpCollectImpl.createServerKeyVerifier(ftpProtocol));
+    }
+
+    @Test
+    void serverKeyVerifierAllowsExplicitVerificationOptOut() {
+        FtpProtocol ftpProtocol = FtpProtocol.builder()
+                .insecureSkipVerify("true")
+                .build();
+
+        assertSame(
+                AcceptAllServerKeyVerifier.INSTANCE,
+                FtpCollectImpl.createServerKeyVerifier(ftpProtocol));
+    }
+
+    @Test
+    void preCheckRejectsMalformedVerificationOptOut() {
+        FtpProtocol ftpProtocol = FtpProtocol.builder()
+                .host("sftp.example.com")
+                .port("22")
+                .direction("/data")
+                .timeout("3000")
+                .ssl("true")
+                .username("admin")
+                .password("secret")
+                .hostKeyFingerprint(KeyUtils.getFingerPrint(
+                        BuiltinDigests.sha256,
+                        generateEcPublicKey()))
+                .insecureSkipVerify("enabled")
+                .build();
+        Metrics metrics = new Metrics();
+        metrics.setFtp(ftpProtocol);
 
+        assertThrows(IllegalArgumentException.class, () -> 
ftpCollectImpl.preCheck(metrics));
+    }
+
+    @Test
+    void preCheckFailsClosedForSftpWithoutHostKeyPolicy() {
+        FtpProtocol ftpProtocol = FtpProtocol.builder()
+                .host("sftp.example.com")
+                .port("22")
+                .direction("/data")
+                .timeout("3000")
+                .ssl("true")
+                .username("admin")
+                .password("secret")
+                .build();
+        Metrics metrics = new Metrics();
+        metrics.setFtp(ftpProtocol);
+
+        IllegalArgumentException exception = assertThrows(
+                IllegalArgumentException.class,
+                () -> ftpCollectImpl.preCheck(metrics));
+
+        assertTrue(exception.getMessage().contains("host key fingerprint is 
required"));
+    }
+
+    private static java.security.PublicKey generateEcPublicKey() {
+        try {
+            KeyPairGenerator keyPairGenerator = 
KeyPairGenerator.getInstance("EC");
+            keyPairGenerator.initialize(256);
+            return keyPairGenerator.generateKeyPair().getPublic();
+        } catch (Exception exception) {
+            throw new IllegalStateException(exception);
+        }
+    }
 }
diff --git 
a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/FtpProtocol.java
 
b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/FtpProtocol.java
index 3767620056..7742408da0 100644
--- 
a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/FtpProtocol.java
+++ 
b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/FtpProtocol.java
@@ -20,6 +20,9 @@ package org.apache.hertzbeat.common.entity.job.protocol;
 import static org.apache.hertzbeat.common.util.IpDomainUtil.validPort;
 import static org.apache.hertzbeat.common.util.IpDomainUtil.validateIpDomain;
 
+import java.util.Arrays;
+import java.util.List;
+import java.util.regex.Pattern;
 import lombok.AllArgsConstructor;
 import lombok.Builder;
 import lombok.Data;
@@ -35,6 +38,10 @@ import org.apache.hertzbeat.common.util.CommonUtil;
 @AllArgsConstructor
 @NoArgsConstructor
 public class FtpProtocol implements CommonRequestProtocol, Protocol {
+
+    private static final Pattern SHA256_FINGERPRINT_PATTERN =
+            Pattern.compile("SHA256:[A-Za-z0-9+/]{43}=?");
+    private static final String UNRESOLVED_SSL_PLACEHOLDER = "^_^ssl^_^";
     /**
      * Peer host ip or domain name
      */
@@ -71,19 +78,84 @@ public class FtpProtocol implements CommonRequestProtocol, 
Protocol {
      */
     private String ssl = "false";
 
+    /**
+     * Expected SFTP server host key fingerprints, separated by commas or line
+     * breaks, for example SHA256:base64.
+     */
+    private String hostKeyFingerprint;
+
+    /**
+     * Whether SFTP host key verification is explicitly disabled.
+     */
+    private String insecureSkipVerify;
+
     @Override
     public boolean isInvalid() {
-        if (!validateIpDomain(host) || !validPort(port) || 
StringUtils.isBlank(direction) || StringUtils.isBlank(timeout)) {
-            return true;
+        return validationError() != null;
+    }
+
+    /**
+     * Validate the complete FTP/SFTP protocol contract used by collectors.
+     *
+     * @return a safe operator-facing error, or {@code null} when valid
+     */
+    public String validationError() {
+        if (!validateIpDomain(host)) {
+            return "Ftp Protocol host is invalid.";
+        }
+        if (!validPort(port)) {
+            return "Ftp Protocol port is invalid.";
         }
-        if (!CommonUtil.isNumeric(timeout)) {
-            return true;
+        if (StringUtils.isBlank(direction)) {
+            return "Ftp Protocol direction is required.";
+        }
+        if (StringUtils.isBlank(timeout) || !CommonUtil.isNumeric(timeout)) {
+            return "Ftp Protocol timeout must be numeric.";
+        }
+        if (UNRESOLVED_SSL_PLACEHOLDER.equals(ssl)) {
+            return null;
         }
         if (StringUtils.isNotBlank(ssl)
                 && !"true".equalsIgnoreCase(ssl)
                 && !"false".equalsIgnoreCase(ssl)) {
-            return true;
+            return "Ftp Protocol SFTP option must be true or false.";
+        }
+        if (!"true".equalsIgnoreCase(ssl)) {
+            return null;
+        }
+        if (StringUtils.isNotBlank(insecureSkipVerify)
+                && !"true".equalsIgnoreCase(insecureSkipVerify)
+                && !"false".equalsIgnoreCase(insecureSkipVerify)) {
+            return "Sftp Protocol skip-verification option must be true or 
false.";
+        }
+        if (StringUtils.isAnyBlank(username, password)) {
+            return "Sftp Protocol username and password are required.";
+        }
+        if ("true".equalsIgnoreCase(insecureSkipVerify)) {
+            return null;
+        }
+        if (StringUtils.isBlank(hostKeyFingerprint)) {
+            return "Sftp Protocol host key fingerprint is required unless 
verification is explicitly skipped.";
+        }
+        if (!hasValidHostKeyFingerprints()) {
+            return "Sftp Protocol host key fingerprints must use the 
SHA256:base64 format.";
+        }
+        return null;
+    }
+
+    public boolean hasValidHostKeyFingerprints() {
+        List<String> fingerprints = parseHostKeyFingerprints();
+        return !fingerprints.isEmpty()
+                && fingerprints.stream().allMatch(value -> 
SHA256_FINGERPRINT_PATTERN.matcher(value).matches());
+    }
+
+    public List<String> parseHostKeyFingerprints() {
+        if (StringUtils.isBlank(hostKeyFingerprint)) {
+            return List.of();
         }
-        return "true".equalsIgnoreCase(ssl) && 
StringUtils.isAnyBlank(username, password);
+        return Arrays.stream(hostKeyFingerprint.split("[,;\\r\\n]+"))
+                .map(String::trim)
+                .filter(StringUtils::isNotEmpty)
+                .toList();
     }
 }
diff --git 
a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/FtpProtocolTest.java
 
b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/FtpProtocolTest.java
index b17795b246..a60f25f9b6 100644
--- 
a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/FtpProtocolTest.java
+++ 
b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/FtpProtocolTest.java
@@ -20,10 +20,14 @@ package org.apache.hertzbeat.common.entity.job.protocol;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
+import org.apache.hertzbeat.common.util.JsonUtil;
 import org.junit.jupiter.api.Test;
 
 class FtpProtocolTest {
 
+    private static final String VALID_SHA256_FINGERPRINT =
+            "SHA256:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
+
     @Test
     void isInvalidValidAnonymousFtp() {
         FtpProtocol protocol = FtpProtocol.builder()
@@ -36,6 +40,34 @@ class FtpProtocolTest {
         assertFalse(protocol.isInvalid());
     }
 
+    @Test
+    void isValidPlainFtpWhenSftpOnlyOptionRemainsUnresolved() {
+        FtpProtocol protocol = FtpProtocol.builder()
+                .host("ftp.example.com")
+                .port("21")
+                .direction("/")
+                .timeout("3000")
+                .ssl("false")
+                .insecureSkipVerify("^_^insecureSkipVerify^_^")
+                .build();
+
+        assertFalse(protocol.isInvalid());
+    }
+
+    @Test
+    void isValidLegacyFtpWhenSslOptionRemainsUnresolved() {
+        FtpProtocol protocol = FtpProtocol.builder()
+                .host("ftp.example.com")
+                .port("21")
+                .direction("/")
+                .timeout("3000")
+                .ssl("^_^ssl^_^")
+                .insecureSkipVerify("^_^insecureSkipVerify^_^")
+                .build();
+
+        assertFalse(protocol.isInvalid());
+    }
+
     @Test
     void isInvalidValidSftp() {
         FtpProtocol protocol = FtpProtocol.builder()
@@ -46,6 +78,7 @@ class FtpProtocolTest {
                 .ssl("true")
                 .username("admin")
                 .password("secret")
+                .hostKeyFingerprint(VALID_SHA256_FINGERPRINT)
                 .build();
         assertFalse(protocol.isInvalid());
     }
@@ -85,6 +118,67 @@ class FtpProtocolTest {
         assertTrue(protocol.isInvalid());
     }
 
+    @Test
+    void isInvalidSftpWithoutHostIdentityConfiguration() {
+        FtpProtocol protocol = FtpProtocol.builder()
+                .host("sftp.example.com")
+                .port("22")
+                .direction("/data")
+                .timeout("3000")
+                .ssl("true")
+                .username("admin")
+                .password("secret")
+                .build();
+        assertTrue(protocol.isInvalid());
+    }
+
+    @Test
+    void isInvalidSftpWhenSkipVerificationOptionRemainsUnresolved() {
+        FtpProtocol protocol = FtpProtocol.builder()
+                .host("sftp.example.com")
+                .port("22")
+                .direction("/data")
+                .timeout("3000")
+                .ssl("true")
+                .username("admin")
+                .password("secret")
+                .hostKeyFingerprint(VALID_SHA256_FINGERPRINT)
+                .insecureSkipVerify("^_^insecureSkipVerify^_^")
+                .build();
+
+        assertTrue(protocol.isInvalid());
+    }
+
+    @Test
+    void isValidSftpWithExplicitVerificationOptOut() {
+        FtpProtocol protocol = FtpProtocol.builder()
+                .host("sftp.example.com")
+                .port("22")
+                .direction("/data")
+                .timeout("3000")
+                .ssl("true")
+                .username("admin")
+                .password("secret")
+                .insecureSkipVerify("true")
+                .build();
+        assertFalse(protocol.isInvalid());
+    }
+
+    @Test
+    void isInvalidSftpWithMalformedHostKeyFingerprint() {
+        FtpProtocol protocol = FtpProtocol.builder()
+                .host("sftp.example.com")
+                .port("22")
+                .direction("/data")
+                .timeout("3000")
+                .ssl("true")
+                .username("admin")
+                .password("secret")
+                .hostKeyFingerprint("SHA256:not-a-valid-fingerprint")
+                .build();
+        assertTrue(protocol.isInvalid());
+    }
+
     @Test
     void isInvalidInvalidTimeout() {
         FtpProtocol protocol = FtpProtocol.builder()
@@ -95,4 +189,23 @@ class FtpProtocolTest {
                 .build();
         assertTrue(protocol.isInvalid());
     }
+
+    @Test
+    void serializationDoesNotExposeComputedValidationProperties() {
+        FtpProtocol protocol = FtpProtocol.builder()
+                .host("sftp.example.com")
+                .port("22")
+                .direction("/data")
+                .timeout("3000")
+                .ssl("true")
+                .username("admin")
+                .password("secret")
+                .hostKeyFingerprint(VALID_SHA256_FINGERPRINT)
+                .build();
+
+        String json = JsonUtil.toJson(protocol);
+
+        assertFalse(json.contains("validationError"));
+        assertFalse(json.contains("parsedHostKeyFingerprints"));
+    }
 }
diff --git a/hertzbeat-manager/src/main/resources/define/app-ftp.yml 
b/hertzbeat-manager/src/main/resources/define/app-ftp.yml
index 1fabc0ad1b..1ac76be185 100644
--- a/hertzbeat-manager/src/main/resources/define/app-ftp.yml
+++ b/hertzbeat-manager/src/main/resources/define/app-ftp.yml
@@ -106,6 +106,30 @@ params:
     type: boolean
     # required-true or false
     required: true
+  - field: hostKeyFingerprint
+    name:
+      zh-CN: SFTP主机密钥指纹(未跳过验证时必填,可填写多个)
+      en-US: SFTP Host Key Fingerprints (Required Unless Verification Is 
Skipped)
+      ja-JP: SFTPホストキーフィンガープリント(検証をスキップしない場合は必須)
+    type: textarea
+    placeholder: 'SHA256:... (one per line; verify through a trusted channel)'
+    required: false
+    hide: true
+    depend:
+      ssl:
+        - true
+  - field: insecureSkipVerify
+    name:
+      zh-CN: 危险:临时跳过SFTP主机密钥验证
+      en-US: 'DANGER: Temporarily Skip SFTP Host Key Verification'
+      ja-JP: 危険:SFTPホストキー検証を一時的にスキップ
+    type: boolean
+    defaultValue: false
+    required: false
+    hide: true
+    depend:
+      ssl:
+        - true
 # collect metrics config list
 metrics:
   # metrics - basic
@@ -144,3 +168,5 @@ metrics:
       direction: ^_^direction^_^
       timeout: ^_^timeout^_^
       ssl: ^_^ssl^_^
+      hostKeyFingerprint: ^_^hostKeyFingerprint^_^
+      insecureSkipVerify: ^_^insecureSkipVerify^_^
diff --git a/home/docs/help/ftp.md b/home/docs/help/ftp.md
index ae0f1aca88..0d87457beb 100644
--- a/home/docs/help/ftp.md
+++ b/home/docs/help/ftp.md
@@ -20,10 +20,36 @@ keywords: [ open source monitoring tool, open source ftp 
server monitoring tool,
 | Timeout             | Timeout for connecting to FTP server.                  
                                                                  |
 | Username            | Username for connecting to the FTP server, optional.   
                                                                  |
 | Password            | Password for connecting to the FTP server, optional.   
                                                                  |
+| SFTP                 | Use SFTP instead of FTP. SFTP requires a username and 
password.                                                           |
+| Host key fingerprints | Trusted SFTP server SHA-256 fingerprints, one per 
line or separated by commas. Required unless verification is explicitly 
skipped. |
+| Skip host key verification | **Dangerous option.** Use only for a controlled 
diagnostic; it does not authenticate the SFTP server.                         |
 | Collection interval | Interval time of monitor periodic data collection, 
unit: second, and the minimum interval that can be set is 30 seconds. |
 | Bind Tags           | Used to classify and manage monitoring resources.      
                                                                  |
 | Description remarks | For more information about identifying and describing 
this monitoring, users can note information here.                  |
 
+## SFTP host key verification
+
+HertzBeat accepts only the configured SFTP host keys. Obtain the server keys,
+then verify their fingerprints through a trusted channel such as the server
+console, configuration management, or an administrator. `ssh-keyscan` alone
+does not authenticate a server.
+
+```shell
+ssh-keyscan -p 22 sftp.example.com > /tmp/sftp-host-keys
+ssh-keygen -lf /tmp/sftp-host-keys -E sha256
+```
+
+Copy the verified `SHA256:...` values into **SFTP Host Key Fingerprints**. The
+field accepts one value per line or comma-separated values.
+
+For a planned host-key rotation, verify the new key first, add both the current
+and new fingerprints, rotate the server key, and remove the old fingerprint
+only after all HertzBeat collectors use the new key.
+
+SFTP monitors and imported configurations must pin at least one fingerprint
+unless the operator explicitly selects the dangerous skip-verification option.
+HertzBeat does not enable that option automatically.
+
 ### Collection Metrics
 
 #### Metrics Set:Basic
diff --git a/home/docs/start/upgrade.md b/home/docs/start/upgrade.md
index 4fbf39648b..a83e6f23ff 100644
--- a/home/docs/start/upgrade.md
+++ b/home/docs/start/upgrade.md
@@ -16,6 +16,24 @@ Apache HertzBeat's metadata information is stored in H2 or 
Mysql, PostgreSQL rel
 
 ## Breaking Changes In 1.9.0
 
+### SFTP monitors require an explicit host-key policy
+
+1.9.0 stops accepting any SFTP server key by default. Every SFTP monitor must
+either pin one or more trusted `SHA256:...` host-key fingerprints or explicitly
+select the dangerous temporary skip-verification option.
+
+This is a fail-closed breaking change. HertzBeat does not automatically enable
+skip verification for 1.8.x monitors, imports, or direct API/SQL-created rows.
+Before or immediately after upgrading, edit each SFTP monitor and:
+
+1. obtain the server key and verify its fingerprint through a trusted channel;
+2. add the verified fingerprint to **SFTP Host Key Fingerprints**; and
+3. use the skip-verification option only as a short-lived recovery measure.
+
+Until one of those policies is configured, the affected SFTP monitor reports a
+configuration failure and does not connect. Plain FTP monitors are unchanged.
+See [FTP Monitor](../help/ftp) for fingerprint acquisition and key rotation.
+
 ### Observability (OTLP / logs / traces) API paths moved
 
 1.9.0 consolidates the 1.8.x log module into `hertzbeat-observability`. 
Metrics, logs and traces now share one ingestion prefix 
(`/api/otlp/v1/{signal}`) and one query prefix (`/api/observability/**`). Any 
OpenTelemetry Collector, Vector, SDK exporter, script or dashboard that was 
configured against a 1.8.x path must be updated.
diff --git a/home/i18n/zh-cn/docusaurus-plugin-content-docs/current/help/ftp.md 
b/home/i18n/zh-cn/docusaurus-plugin-content-docs/current/help/ftp.md
index aeff3763f2..ef5f6c61b9 100644
--- a/home/i18n/zh-cn/docusaurus-plugin-content-docs/current/help/ftp.md
+++ b/home/i18n/zh-cn/docusaurus-plugin-content-docs/current/help/ftp.md
@@ -20,10 +20,32 @@ keywords: [ 开源监控系统, 开源FTP服务器监控工具, 监控FTP指标
 | 超时时间   | 连接FTP服务器超时时间,默认值:1000毫秒。             |
 | 用户名    | 连接FTP服务的用户名, 可选。                     |
 | 密码     | 连接FTP服务的密码,可选。                       |
+| 启用SFTP | 使用SFTP替代FTP;SFTP必须配置用户名和密码。 |
+| SFTP主机密钥指纹 | 可信的SFTP服务器SHA-256指纹,每行一个或使用逗号分隔;除非显式跳过验证,否则必填。 |
+| 跳过主机密钥验证 | **危险选项。** 仅应用于受控诊断;启用后无法验证SFTP服务器身份。 |
 | 采集间隔   | 监控周期性采集数据间隔时间,单位秒,可设置的最小间隔为30秒。      |
 | 绑定标签   | 用于对监控资源进行分类管理。                       |
 | 描述备注   | 更多标识和描述此监控的备注信息,用户可以在这里备注信息。         |
 
+## SFTP主机密钥验证
+
+HertzBeat只接受已配置的SFTP主机密钥。请先获取服务器密钥,再通过服务器控制台、
+配置管理系统或管理员等可信渠道核对指纹。单独使用`ssh-keyscan`不能证明服务器身份。
+
+```shell
+ssh-keyscan -p 22 sftp.example.com > /tmp/sftp-host-keys
+ssh-keygen -lf /tmp/sftp-host-keys -E sha256
+```
+
+将核对后的`SHA256:...`填写到“SFTP主机密钥指纹”中。可以每行填写一个,也可以用
+逗号分隔。
+
+计划轮换主机密钥时,先通过可信渠道核对新密钥,将旧、新指纹同时加入配置,再轮换
+服务器密钥;所有HertzBeat采集器都使用新密钥后,才能删除旧指纹。
+
+SFTP监控和导入的配置必须至少固定一个指纹;只有操作员显式选择危险的跳过验证
+选项时才允许省略。HertzBeat不会自动启用该选项。
+
 ### 采集指标
 
 #### 指标集合:概要
diff --git 
a/home/i18n/zh-cn/docusaurus-plugin-content-docs/current/start/upgrade.md 
b/home/i18n/zh-cn/docusaurus-plugin-content-docs/current/start/upgrade.md
index d36d3698f2..b6026a6fad 100644
--- a/home/i18n/zh-cn/docusaurus-plugin-content-docs/current/start/upgrade.md
+++ b/home/i18n/zh-cn/docusaurus-plugin-content-docs/current/start/upgrade.md
@@ -16,6 +16,21 @@ HertzBeat 的元数据信息保存在 H2 或 Mysql, PostgreSQL 关系型数据
 
 ## 1.9.0 不兼容变更
 
+### SFTP监控必须显式配置主机密钥策略
+
+1.9.0不再默认接受任意SFTP服务器密钥。每个SFTP监控必须配置一个或多个可信的
+`SHA256:...`主机密钥指纹,或者由操作员显式选择危险的临时跳过验证选项。
+
+这是一个失败关闭的不兼容变更。HertzBeat不会为1.8.x监控、导入配置或通过API/SQL
+直接创建的记录自动启用跳过验证。升级前或升级后应立即编辑每个SFTP监控:
+
+1. 获取服务器密钥,并通过可信渠道核对指纹;
+2. 将核对后的指纹添加到“SFTP主机密钥指纹”;
+3. 仅把跳过验证选项用于短期故障恢复。
+
+完成上述任一策略配置前,受影响的SFTP监控会报告配置失败且不会建立连接。普通FTP
+监控不受影响。指纹获取与密钥轮换方法请参阅[FTP监控](../help/ftp)。
+
 ### 可观测(OTLP / 日志 / 链路)接口路径变更
 
 1.9.0 将 1.8.x 的日志模块合并为 `hertzbeat-observability`,指标、日志、链路统一使用 
`/api/otlp/v1/{signal}` 接收、`/api/observability/**` 查询。所有按 1.8.x 路径配置的 
OpenTelemetry Collector、Vector、SDK exporter、脚本或看板都需要更新。
diff --git 
a/web-app/src/app/routes/monitor/monitor-form/monitor-form.component.spec.ts 
b/web-app/src/app/routes/monitor/monitor-form/monitor-form.component.spec.ts
index 7603f02c09..aec3b710d4 100644
--- a/web-app/src/app/routes/monitor/monitor-form/monitor-form.component.spec.ts
+++ b/web-app/src/app/routes/monitor/monitor-form/monitor-form.component.spec.ts
@@ -99,4 +99,54 @@ describe('MonitorFormComponent', () => {
     expect(payloadParam.display).toBeTrue();
     expect(component.hasAdvancedParams).toBeTrue();
   });
+
+  it('should reevaluate dependent advanced fields when a boolean parent 
changes', () => {
+    const fingerprintDefine = new ParamDefine();
+    fingerprintDefine.field = 'hostKeyFingerprint';
+    fingerprintDefine.name = 'SFTP Host Key Fingerprints';
+    fingerprintDefine.type = 'textarea';
+    (fingerprintDefine as any).depend = {
+      ssl: [true]
+    };
+
+    const fingerprintParam = new Param();
+    fingerprintParam.field = 'hostKeyFingerprint';
+    fingerprintParam.paramValue = 'SHA256:test';
+    fingerprintParam.display = false;
+
+    component.monitor.app = 'ftp';
+    component.paramDefines = [];
+    component.params = [];
+    component.sdDefines = [];
+    component.sdParams = [];
+    component.advancedParamDefines = [fingerprintDefine];
+    component.advancedParams = [fingerprintParam];
+
+    component.onParamBooleanChanged(true, 'ssl');
+    expect(fingerprintParam.display).toBeTrue();
+    expect(component.hasAdvancedParams).toBeTrue();
+
+    component.onParamBooleanChanged(false, 'ssl');
+    expect(fingerprintParam.display).toBeFalse();
+    expect(fingerprintParam.paramValue).toBeNull();
+    expect(component.hasAdvancedParams).toBeFalse();
+  });
+
+  it('should not treat a persisted false string as an enabled boolean', () => {
+    const portParam = new Param();
+    portParam.field = 'port';
+    portParam.paramValue = 21;
+
+    component.monitor.app = 'ftp';
+    component.params = [portParam];
+    component.paramDefines = [];
+    component.sdDefines = [];
+    component.sdParams = [];
+    component.advancedParamDefines = [];
+    component.advancedParams = [];
+
+    component.onParamBooleanChanged('false', 'ssl');
+
+    expect(portParam.paramValue).toBe(21);
+  });
 });
diff --git 
a/web-app/src/app/routes/monitor/monitor-form/monitor-form.component.ts 
b/web-app/src/app/routes/monitor/monitor-form/monitor-form.component.ts
index e695d1dd88..5545857413 100644
--- a/web-app/src/app/routes/monitor/monitor-form/monitor-form.component.ts
+++ b/web-app/src/app/routes/monitor/monitor-form/monitor-form.component.ts
@@ -198,16 +198,17 @@ export class MonitorFormComponent implements OnChanges {
     this.hostChange.emit(host);
   }
 
-  onParamBooleanChanged(booleanValue: boolean, field: string) {
+  onParamBooleanChanged(booleanValue: boolean | string, field: string) {
+    const enabled = booleanValue === true || 
String(booleanValue).toLowerCase() === 'true';
     if (this.monitor.app === 'api') {
       if (field === 'ssl') {
         const portParam = this.params.find(param => param.field === 'port');
         if (portParam) {
-          if (booleanValue && (portParam.paramValue == null || 
parseInt(portParam.paramValue) === 80)) {
+          if (enabled && (portParam.paramValue == null || 
parseInt(portParam.paramValue) === 80)) {
             portParam.paramValue = 443;
             this.notifySvc.info(this.i18nSvc.fanyi('common.notice'), 
this.i18nSvc.fanyi('monitor.new.notify.change-to-https'));
           }
-          if (!booleanValue && (portParam.paramValue == null || 
parseInt(portParam.paramValue) === 443)) {
+          if (!enabled && (portParam.paramValue == null || 
parseInt(portParam.paramValue) === 443)) {
             portParam.paramValue = 80;
             this.notifySvc.info(this.i18nSvc.fanyi('common.notice'), 
this.i18nSvc.fanyi('monitor.new.notify.change-to-http'));
           }
@@ -217,17 +218,18 @@ export class MonitorFormComponent implements OnChanges {
       if (field === 'ssl') {
         const portParam = this.params.find(param => param.field === 'port');
         if (portParam) {
-          if (booleanValue && (portParam.paramValue == null || 
parseInt(portParam.paramValue) === 21)) {
+          if (enabled && (portParam.paramValue == null || 
parseInt(portParam.paramValue) === 21)) {
             portParam.paramValue = 22;
             this.notifySvc.info(this.i18nSvc.fanyi('common.notice'), 
this.i18nSvc.fanyi('monitor.new.notify.change-to-sftp'));
           }
-          if (!booleanValue && (portParam.paramValue == null || 
parseInt(portParam.paramValue) === 22)) {
+          if (!enabled && (portParam.paramValue == null || 
parseInt(portParam.paramValue) === 22)) {
             portParam.paramValue = 21;
             this.notifySvc.info(this.i18nSvc.fanyi('common.notice'), 
this.i18nSvc.fanyi('monitor.new.notify.change-to-ftp'));
           }
         }
       }
     }
+    this.onDependChanged(String(enabled), field);
   }
 
   onDependChanged(dependValue: string, dependField: string) {


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to