From 6a368854b0c9afc2c4c704536212ff5237ed09bd Mon Sep 17 00:00:00 2001 From: Logic Date: Thu, 30 Jul 2026 18:26:31 +0800 Subject: [PATCH 1/2] maintenance: extend SFTP connection options --- .../collector/collect/ftp/FtpCollectImpl.java | 46 +++++++++- .../collect/ftp/FtpCollectImplTest.java | 38 ++++++++ .../entity/job/protocol/FtpProtocol.java | 24 +++++- .../entity/job/protocol/FtpProtocolTest.java | 30 +++++++ .../SftpHostKeyCompatibilityMigration.java | 86 +++++++++++++++++++ .../src/main/resources/define/app-ftp.yml | 27 ++++++ ...SftpHostKeyCompatibilityMigrationTest.java | 84 ++++++++++++++++++ home/docs/help/ftp.md | 39 +++++++++ .../current/help/ftp.md | 35 ++++++++ 9 files changed, 407 insertions(+), 2 deletions(-) create mode 100644 hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/component/migration/SftpHostKeyCompatibilityMigration.java create mode 100644 hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/component/migration/SftpHostKeyCompatibilityMigrationTest.java 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 03307e1c7a8..7fbfa1793f9 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 @@ -18,7 +18,11 @@ package org.apache.hertzbeat.collector.collect.ftp; import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Objects; @@ -32,7 +36,10 @@ 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.sftp.client.SftpClient; import org.apache.sshd.sftp.client.SftpClientFactory; import org.springframework.util.Assert; @@ -60,6 +67,11 @@ public void preCheck(Metrics metrics) throws IllegalArgumentException{ 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."); + if (Boolean.parseBoolean(ftpProtocol.getSsl()) + && !Boolean.parseBoolean(ftpProtocol.getInsecureSkipVerify())) { + Assert.hasText(ftpProtocol.getHostKeyFingerprint(), + "Sftp Protocol host key fingerprint is required."); + } } @Override @@ -202,6 +214,7 @@ private void handleSftpCollect(CollectRep.MetricsData.Builder builder, Metrics m SshClient client = null; try { client = SshClient.setUpDefaultClient(); + client.setServerKeyVerifier(createServerKeyVerifier(ftpProtocol)); session = connect(client, ftpProtocol); sftpClient = SftpClientFactory.instance().createSftpClient(session); Map valueMap = collectValue(sftpClient, ftpProtocol); @@ -229,4 +242,35 @@ private void handleSftpCollect(CollectRep.MetricsData.Builder builder, Metrics m } } } -} \ No newline at end of file + + static ServerKeyVerifier createServerKeyVerifier(FtpProtocol ftpProtocol) { + if (Boolean.parseBoolean(ftpProtocol.getInsecureSkipVerify())) { + log.warn("[SFTPClient] host key verification is disabled for {}:{}; " + + "configure trusted host key fingerprints before disabling compatibility mode", + ftpProtocol.getHost(), ftpProtocol.getPort()); + 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."); + List expectedFingerprints = Arrays.stream(ftpProtocol.getHostKeyFingerprint() + .split("[,;\\r\\n]+")) + .map(String::trim) + .filter(value -> !value.isEmpty()) + .toList(); + Assert.notEmpty(expectedFingerprints, + "Sftp Protocol host key fingerprint list must not be empty."); + return (clientSession, remoteAddress, serverKey) -> { + String actualFingerprint = KeyUtils.getFingerPrint(serverKey); + boolean matches = actualFingerprint != null && expectedFingerprints.stream() + .anyMatch(expectedFingerprint -> MessageDigest.isEqual( + expectedFingerprint.getBytes(StandardCharsets.UTF_8), + actualFingerprint.getBytes(StandardCharsets.UTF_8))); + if (!matches) { + log.warn("[SFTPClient] server host key did not match for {}:{}", + ftpProtocol.getHost(), ftpProtocol.getPort()); + } + return matches; + }; + } +} 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 5af290d60db..9f8d2967876 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,22 @@ 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.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.common.config.keys.KeyUtils; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -143,5 +150,36 @@ void testAnonymousCollect() throws IOException { } + @Test + void serverKeyVerifierSupportsHostKeyRotationWindow() throws Exception { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC"); + keyPairGenerator.initialize(256); + var currentKey = keyPairGenerator.generateKeyPair().getPublic(); + var nextKey = keyPairGenerator.generateKeyPair().getPublic(); + var unrelatedKey = keyPairGenerator.generateKeyPair().getPublic(); + FtpProtocol ftpProtocol = FtpProtocol.builder() + .host("sftp.example.com") + .port("22") + .hostKeyFingerprint(KeyUtils.getFingerPrint(currentKey) + + System.lineSeparator() + + KeyUtils.getFingerPrint(nextKey)) + .build(); + + ServerKeyVerifier verifier = FtpCollectImpl.createServerKeyVerifier(ftpProtocol); + + assertTrue(verifier.verifyServerKey(null, null, currentKey)); + assertTrue(verifier.verifyServerKey(null, null, nextKey)); + assertFalse(verifier.verifyServerKey(null, null, unrelatedKey)); + } + @Test + void serverKeyVerifierAllowsExplicitVerificationOptOut() { + FtpProtocol ftpProtocol = FtpProtocol.builder() + .insecureSkipVerify("true") + .build(); + + assertSame( + AcceptAllServerKeyVerifier.INSTANCE, + FtpCollectImpl.createServerKeyVerifier(ftpProtocol)); + } } 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 37676200567..fb7777bddc1 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 @@ -71,6 +71,17 @@ 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)) { @@ -84,6 +95,17 @@ public boolean isInvalid() { && !"false".equalsIgnoreCase(ssl)) { return true; } - return "true".equalsIgnoreCase(ssl) && StringUtils.isAnyBlank(username, password); + if (StringUtils.isNotBlank(insecureSkipVerify) + && !"true".equalsIgnoreCase(insecureSkipVerify) + && !"false".equalsIgnoreCase(insecureSkipVerify)) { + return true; + } + if (!"true".equalsIgnoreCase(ssl)) { + return false; + } + if (StringUtils.isAnyBlank(username, password)) { + return true; + } + return !"true".equalsIgnoreCase(insecureSkipVerify) && StringUtils.isBlank(hostKeyFingerprint); } } 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 b17795b2469..fdc96d46df7 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 @@ -46,6 +46,7 @@ void isInvalidValidSftp() { .ssl("true") .username("admin") .password("secret") + .hostKeyFingerprint("SHA256:expected") .build(); assertFalse(protocol.isInvalid()); } @@ -85,6 +86,35 @@ void isInvalidSftpWithoutPassword() { 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 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 isInvalidInvalidTimeout() { FtpProtocol protocol = FtpProtocol.builder() diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/component/migration/SftpHostKeyCompatibilityMigration.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/component/migration/SftpHostKeyCompatibilityMigration.java new file mode 100644 index 00000000000..560b6bb36b6 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/component/migration/SftpHostKeyCompatibilityMigration.java @@ -0,0 +1,86 @@ +/* + * 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.hertzbeat.manager.component.migration; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.manager.Monitor; +import org.apache.hertzbeat.common.entity.manager.Param; +import org.apache.hertzbeat.manager.dao.MonitorDao; +import org.apache.hertzbeat.manager.dao.ParamDao; +import org.springframework.boot.CommandLineRunner; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +/** + * Keeps pre-pinning SFTP monitors available until operators configure a trusted + * host key. New monitors do not pass through this one-time compatibility path. + */ +@Slf4j +@Component +@RequiredArgsConstructor +@Order(Ordered.HIGHEST_PRECEDENCE) +public class SftpHostKeyCompatibilityMigration implements CommandLineRunner { + + private static final String FTP_APP = "ftp"; + private static final String SFTP_FIELD = "ssl"; + private static final String HOST_KEY_FIELD = "hostKeyFingerprint"; + private static final String INSECURE_FIELD = "insecureSkipVerify"; + + private final MonitorDao monitorDao; + private final ParamDao paramDao; + + @Override + @Transactional + public void run(String... args) { + int migratedMonitorCount = 0; + for (Monitor monitor : monitorDao.findMonitorsByAppEquals(FTP_APP)) { + List params = paramDao.findParamsByMonitorId(monitor.getId()); + if (isEnabledSftp(params) && hasNoHostKeyPolicy(params)) { + paramDao.save(Param.builder() + .monitorId(monitor.getId()) + .field(INSECURE_FIELD) + .paramValue(Boolean.TRUE.toString()) + .type(CommonConstants.TYPE_STRING) + .build()); + migratedMonitorCount++; + } + } + if (migratedMonitorCount > 0) { + log.warn("Enabled temporary SFTP host-key compatibility for {} existing monitor(s). " + + "Verify and pin the server fingerprints, then disable the unsafe option.", + migratedMonitorCount); + } + } + + private boolean isEnabledSftp(List params) { + return params.stream() + .anyMatch(param -> SFTP_FIELD.equals(param.getField()) + && Boolean.parseBoolean(param.getParamValue())); + } + + private boolean hasNoHostKeyPolicy(List params) { + return params.stream() + .noneMatch(param -> HOST_KEY_FIELD.equals(param.getField()) + || INSECURE_FIELD.equals(param.getField())); + } +} diff --git a/hertzbeat-manager/src/main/resources/define/app-ftp.yml b/hertzbeat-manager/src/main/resources/define/app-ftp.yml index 1fabc0ad1bc..7df6bfa1b8c 100644 --- a/hertzbeat-manager/src/main/resources/define/app-ftp.yml +++ b/hertzbeat-manager/src/main/resources/define/app-ftp.yml @@ -106,6 +106,31 @@ params: type: boolean # required-true or false required: true + - field: hostKeyFingerprint + name: + zh-CN: SFTP主机密钥指纹(必填,可填写多个) + en-US: SFTP Host Key Fingerprints (Required, Multiple Allowed) + ja-JP: SFTPホストキーフィンガープリント(必須、複数可) + type: textarea + limit: 2048 + 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 +169,5 @@ metrics: direction: ^_^direction^_^ timeout: ^_^timeout^_^ ssl: ^_^ssl^_^ + hostKeyFingerprint: ^_^hostKeyFingerprint^_^ + insecureSkipVerify: ^_^insecureSkipVerify^_^ diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/component/migration/SftpHostKeyCompatibilityMigrationTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/component/migration/SftpHostKeyCompatibilityMigrationTest.java new file mode 100644 index 00000000000..4d9ed69db71 --- /dev/null +++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/component/migration/SftpHostKeyCompatibilityMigrationTest.java @@ -0,0 +1,84 @@ +/* + * 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.hertzbeat.manager.component.migration; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.List; +import org.apache.hertzbeat.common.entity.manager.Monitor; +import org.apache.hertzbeat.common.entity.manager.Param; +import org.apache.hertzbeat.manager.dao.MonitorDao; +import org.apache.hertzbeat.manager.dao.ParamDao; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class SftpHostKeyCompatibilityMigrationTest { + + @Mock + private MonitorDao monitorDao; + + @Mock + private ParamDao paramDao; + + @InjectMocks + private SftpHostKeyCompatibilityMigration migration; + + @Test + void preservesAnExistingSftpMonitorWithAnExplicitTemporaryCompatibilityFlag() throws Exception { + Monitor monitor = Monitor.builder().id(100L).app("ftp").build(); + when(monitorDao.findMonitorsByAppEquals("ftp")).thenReturn(List.of(monitor)); + when(paramDao.findParamsByMonitorId(100L)).thenReturn(List.of( + Param.builder().monitorId(100L).field("ssl").paramValue("true").build())); + ArgumentCaptor paramCaptor = ArgumentCaptor.forClass(Param.class); + + migration.run(); + + verify(paramDao).save(paramCaptor.capture()); + Param migrated = paramCaptor.getValue(); + assertEquals(100L, migrated.getMonitorId()); + assertEquals("insecureSkipVerify", migrated.getField()); + assertEquals("true", migrated.getParamValue()); + } + + @Test + void leavesPinnedAndAlreadyMigratedSftpMonitorsUnchanged() throws Exception { + Monitor pinned = Monitor.builder().id(101L).app("ftp").build(); + Monitor migrated = Monitor.builder().id(102L).app("ftp").build(); + when(monitorDao.findMonitorsByAppEquals("ftp")).thenReturn(List.of(pinned, migrated)); + when(paramDao.findParamsByMonitorId(101L)).thenReturn(List.of( + Param.builder().monitorId(101L).field("ssl").paramValue("true").build(), + Param.builder().monitorId(101L).field("hostKeyFingerprint") + .paramValue("SHA256:pinned").build())); + when(paramDao.findParamsByMonitorId(102L)).thenReturn(List.of( + Param.builder().monitorId(102L).field("ssl").paramValue("true").build(), + Param.builder().monitorId(102L).field("insecureSkipVerify") + .paramValue("true").build())); + + migration.run(); + + verify(paramDao, never()).save(org.mockito.ArgumentMatchers.any()); + } +} diff --git a/home/docs/help/ftp.md b/home/docs/help/ftp.md index ae0f1aca88a..13787e6ad4e 100644 --- a/home/docs/help/ftp.md +++ b/home/docs/help/ftp.md @@ -20,10 +20,49 @@ 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 temporary unsafe compatibility is enabled. | +| Skip host key verification | **Dangerous temporary option.** It keeps compatibility but 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. + +### Upgrade from an earlier release + +At the first startup after this change, existing SFTP monitors that have no +host-key setting are explicitly migrated to the temporary +`insecureSkipVerify` compatibility option so collection does not stop. HertzBeat +logs a warning for the migration and whenever such a monitor connects. + +For every migrated monitor: + +1. Verify the server fingerprint through a trusted channel. +2. Add it to **SFTP Host Key Fingerprints**. +3. Turn off **DANGER: Temporarily Skip SFTP Host Key Verification**. +4. Run detection and confirm collection succeeds. + +New SFTP monitors and imported configurations must pin at least one fingerprint +unless the operator explicitly selects the dangerous temporary option. + ### Collection Metrics #### Metrics Set:Basic 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 aeff3763f2a..4da89fe373f 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,45 @@ 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会显式为其加入 +临时`insecureSkipVerify`兼容选项,避免采集中断。迁移时以及该监控每次连接时都会 +记录警告。 + +请逐个处理迁移后的监控: + +1. 通过可信渠道核对服务器指纹。 +2. 将指纹填写到“SFTP主机密钥指纹”。 +3. 关闭“危险:临时跳过SFTP主机密钥验证”。 +4. 执行探测并确认采集成功。 + +新建SFTP监控和导入的配置必须至少固定一个指纹;只有操作员显式选择危险的临时 +选项时才允许跳过验证。 + ### 采集指标 #### 指标集合:概要 From a920268d2bd358971ace5834eedd7aef6a0e8eb3 Mon Sep 17 00:00:00 2001 From: Logic Date: Tue, 4 Aug 2026 20:59:22 +0800 Subject: [PATCH 2/2] Tighten SFTP host key configuration --- .../collector/collect/ftp/FtpCollectImpl.java | 16 ++-- .../collect/ftp/FtpCollectImplTest.java | 24 +++++- .../entity/job/protocol/FtpProtocol.java | 25 ++++++ .../entity/job/protocol/FtpProtocolTest.java | 20 ++++- .../SftpHostKeyCompatibilityMigration.java | 86 ------------------- .../src/main/resources/define/app-ftp.yml | 6 +- ...SftpHostKeyCompatibilityMigrationTest.java | 84 ------------------ home/docs/help/ftp.md | 23 ++--- .../current/help/ftp.md | 21 +---- 9 files changed, 86 insertions(+), 219 deletions(-) delete mode 100644 hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/component/migration/SftpHostKeyCompatibilityMigration.java delete mode 100644 hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/component/migration/SftpHostKeyCompatibilityMigrationTest.java 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 7fbfa1793f9..e1087b81e39 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 @@ -20,7 +20,6 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; -import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -40,6 +39,7 @@ 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; @@ -71,6 +71,8 @@ public void preCheck(Metrics metrics) throws IllegalArgumentException{ && !Boolean.parseBoolean(ftpProtocol.getInsecureSkipVerify())) { Assert.hasText(ftpProtocol.getHostKeyFingerprint(), "Sftp Protocol host key fingerprint is required."); + Assert.isTrue(ftpProtocol.hasValidHostKeyFingerprints(), + "Sftp Protocol host key fingerprints must use the SHA256:base64 format."); } } @@ -246,22 +248,20 @@ private void handleSftpCollect(CollectRep.MetricsData.Builder builder, Metrics m static ServerKeyVerifier createServerKeyVerifier(FtpProtocol ftpProtocol) { if (Boolean.parseBoolean(ftpProtocol.getInsecureSkipVerify())) { log.warn("[SFTPClient] host key verification is disabled for {}:{}; " - + "configure trusted host key fingerprints before disabling compatibility mode", + + "configure trusted host key fingerprints and re-enable verification", ftpProtocol.getHost(), ftpProtocol.getPort()); 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."); - List expectedFingerprints = Arrays.stream(ftpProtocol.getHostKeyFingerprint() - .split("[,;\\r\\n]+")) - .map(String::trim) - .filter(value -> !value.isEmpty()) - .toList(); + Assert.isTrue(ftpProtocol.hasValidHostKeyFingerprints(), + "Sftp Protocol host key fingerprints must use the SHA256:base64 format."); + List expectedFingerprints = ftpProtocol.getParsedHostKeyFingerprints(); Assert.notEmpty(expectedFingerprints, "Sftp Protocol host key fingerprint list must not be empty."); return (clientSession, remoteAddress, serverKey) -> { - String actualFingerprint = KeyUtils.getFingerPrint(serverKey); + String actualFingerprint = KeyUtils.getFingerPrint(BuiltinDigests.sha256, serverKey); boolean matches = actualFingerprint != null && expectedFingerprints.stream() .anyMatch(expectedFingerprint -> MessageDigest.isEqual( expectedFingerprint.getBytes(StandardCharsets.UTF_8), 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 9f8d2967876..a8ff10eaf4b 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 @@ -34,6 +34,7 @@ import org.apache.sshd.client.keyverifier.AcceptAllServerKeyVerifier; import org.apache.sshd.client.keyverifier.ServerKeyVerifier; 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; @@ -160,9 +161,9 @@ void serverKeyVerifierSupportsHostKeyRotationWindow() throws Exception { FtpProtocol ftpProtocol = FtpProtocol.builder() .host("sftp.example.com") .port("22") - .hostKeyFingerprint(KeyUtils.getFingerPrint(currentKey) + .hostKeyFingerprint(KeyUtils.getFingerPrint(BuiltinDigests.sha256, currentKey) + System.lineSeparator() - + KeyUtils.getFingerPrint(nextKey)) + + KeyUtils.getFingerPrint(BuiltinDigests.sha256, nextKey)) .build(); ServerKeyVerifier verifier = FtpCollectImpl.createServerKeyVerifier(ftpProtocol); @@ -172,6 +173,25 @@ void serverKeyVerifierSupportsHostKeyRotationWindow() throws Exception { assertFalse(verifier.verifyServerKey(null, null, unrelatedKey)); } + @Test + void serverKeyVerifierAlwaysUsesSha256() throws Exception { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC"); + keyPairGenerator.initialize(256); + var serverKey = keyPairGenerator.generateKeyPair().getPublic(); + FtpProtocol ftpProtocol = FtpProtocol.builder() + .hostKeyFingerprint(KeyUtils.getFingerPrint(BuiltinDigests.sha256, serverKey)) + .build(); + var previousFactory = KeyUtils.getDefaultFingerPrintFactory(); + + try { + KeyUtils.setDefaultFingerPrintFactory(BuiltinDigests.md5); + ServerKeyVerifier verifier = FtpCollectImpl.createServerKeyVerifier(ftpProtocol); + assertTrue(verifier.verifyServerKey(null, null, serverKey)); + } finally { + KeyUtils.setDefaultFingerPrintFactory(previousFactory); + } + } + @Test void serverKeyVerifierAllowsExplicitVerificationOptOut() { FtpProtocol ftpProtocol = FtpProtocol.builder() 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 fb7777bddc1..d3eec1fab87 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 @@ 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,9 @@ @AllArgsConstructor @NoArgsConstructor public class FtpProtocol implements CommonRequestProtocol, Protocol { + + private static final Pattern SHA256_FINGERPRINT_PATTERN = + Pattern.compile("SHA256:[A-Za-z0-9+/]{43}=?"); /** * Peer host ip or domain name */ @@ -106,6 +112,25 @@ public boolean isInvalid() { if (StringUtils.isAnyBlank(username, password)) { return true; } + if (StringUtils.isNotBlank(hostKeyFingerprint) && !hasValidHostKeyFingerprints()) { + return true; + } return !"true".equalsIgnoreCase(insecureSkipVerify) && StringUtils.isBlank(hostKeyFingerprint); } + + public boolean hasValidHostKeyFingerprints() { + List fingerprints = getParsedHostKeyFingerprints(); + return !fingerprints.isEmpty() + && fingerprints.stream().allMatch(value -> SHA256_FINGERPRINT_PATTERN.matcher(value).matches()); + } + + public List getParsedHostKeyFingerprints() { + if (StringUtils.isBlank(hostKeyFingerprint)) { + return List.of(); + } + 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 fdc96d46df7..f9ee725326c 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 @@ -24,6 +24,9 @@ class FtpProtocolTest { + private static final String VALID_SHA256_FINGERPRINT = + "SHA256:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; + @Test void isInvalidValidAnonymousFtp() { FtpProtocol protocol = FtpProtocol.builder() @@ -46,7 +49,7 @@ void isInvalidValidSftp() { .ssl("true") .username("admin") .password("secret") - .hostKeyFingerprint("SHA256:expected") + .hostKeyFingerprint(VALID_SHA256_FINGERPRINT) .build(); assertFalse(protocol.isInvalid()); } @@ -115,6 +118,21 @@ void isValidSftpWithExplicitVerificationOptOut() { 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() diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/component/migration/SftpHostKeyCompatibilityMigration.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/component/migration/SftpHostKeyCompatibilityMigration.java deleted file mode 100644 index 560b6bb36b6..00000000000 --- a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/component/migration/SftpHostKeyCompatibilityMigration.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * 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.hertzbeat.manager.component.migration; - -import java.util.List; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.apache.hertzbeat.common.constants.CommonConstants; -import org.apache.hertzbeat.common.entity.manager.Monitor; -import org.apache.hertzbeat.common.entity.manager.Param; -import org.apache.hertzbeat.manager.dao.MonitorDao; -import org.apache.hertzbeat.manager.dao.ParamDao; -import org.springframework.boot.CommandLineRunner; -import org.springframework.core.Ordered; -import org.springframework.core.annotation.Order; -import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; - -/** - * Keeps pre-pinning SFTP monitors available until operators configure a trusted - * host key. New monitors do not pass through this one-time compatibility path. - */ -@Slf4j -@Component -@RequiredArgsConstructor -@Order(Ordered.HIGHEST_PRECEDENCE) -public class SftpHostKeyCompatibilityMigration implements CommandLineRunner { - - private static final String FTP_APP = "ftp"; - private static final String SFTP_FIELD = "ssl"; - private static final String HOST_KEY_FIELD = "hostKeyFingerprint"; - private static final String INSECURE_FIELD = "insecureSkipVerify"; - - private final MonitorDao monitorDao; - private final ParamDao paramDao; - - @Override - @Transactional - public void run(String... args) { - int migratedMonitorCount = 0; - for (Monitor monitor : monitorDao.findMonitorsByAppEquals(FTP_APP)) { - List params = paramDao.findParamsByMonitorId(monitor.getId()); - if (isEnabledSftp(params) && hasNoHostKeyPolicy(params)) { - paramDao.save(Param.builder() - .monitorId(monitor.getId()) - .field(INSECURE_FIELD) - .paramValue(Boolean.TRUE.toString()) - .type(CommonConstants.TYPE_STRING) - .build()); - migratedMonitorCount++; - } - } - if (migratedMonitorCount > 0) { - log.warn("Enabled temporary SFTP host-key compatibility for {} existing monitor(s). " - + "Verify and pin the server fingerprints, then disable the unsafe option.", - migratedMonitorCount); - } - } - - private boolean isEnabledSftp(List params) { - return params.stream() - .anyMatch(param -> SFTP_FIELD.equals(param.getField()) - && Boolean.parseBoolean(param.getParamValue())); - } - - private boolean hasNoHostKeyPolicy(List params) { - return params.stream() - .noneMatch(param -> HOST_KEY_FIELD.equals(param.getField()) - || INSECURE_FIELD.equals(param.getField())); - } -} diff --git a/hertzbeat-manager/src/main/resources/define/app-ftp.yml b/hertzbeat-manager/src/main/resources/define/app-ftp.yml index 7df6bfa1b8c..8e3836ace40 100644 --- a/hertzbeat-manager/src/main/resources/define/app-ftp.yml +++ b/hertzbeat-manager/src/main/resources/define/app-ftp.yml @@ -108,9 +108,9 @@ params: required: true - field: hostKeyFingerprint name: - zh-CN: SFTP主机密钥指纹(必填,可填写多个) - en-US: SFTP Host Key Fingerprints (Required, Multiple Allowed) - ja-JP: SFTPホストキーフィンガープリント(必須、複数可) + zh-CN: SFTP主机密钥指纹(未跳过验证时必填,可填写多个) + en-US: SFTP Host Key Fingerprints (Required Unless Verification Is Skipped) + ja-JP: SFTPホストキーフィンガープリント(検証をスキップしない場合は必須) type: textarea limit: 2048 placeholder: 'SHA256:... (one per line; verify through a trusted channel)' diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/component/migration/SftpHostKeyCompatibilityMigrationTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/component/migration/SftpHostKeyCompatibilityMigrationTest.java deleted file mode 100644 index 4d9ed69db71..00000000000 --- a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/component/migration/SftpHostKeyCompatibilityMigrationTest.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * 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.hertzbeat.manager.component.migration; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.util.List; -import org.apache.hertzbeat.common.entity.manager.Monitor; -import org.apache.hertzbeat.common.entity.manager.Param; -import org.apache.hertzbeat.manager.dao.MonitorDao; -import org.apache.hertzbeat.manager.dao.ParamDao; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.ArgumentCaptor; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -@ExtendWith(MockitoExtension.class) -class SftpHostKeyCompatibilityMigrationTest { - - @Mock - private MonitorDao monitorDao; - - @Mock - private ParamDao paramDao; - - @InjectMocks - private SftpHostKeyCompatibilityMigration migration; - - @Test - void preservesAnExistingSftpMonitorWithAnExplicitTemporaryCompatibilityFlag() throws Exception { - Monitor monitor = Monitor.builder().id(100L).app("ftp").build(); - when(monitorDao.findMonitorsByAppEquals("ftp")).thenReturn(List.of(monitor)); - when(paramDao.findParamsByMonitorId(100L)).thenReturn(List.of( - Param.builder().monitorId(100L).field("ssl").paramValue("true").build())); - ArgumentCaptor paramCaptor = ArgumentCaptor.forClass(Param.class); - - migration.run(); - - verify(paramDao).save(paramCaptor.capture()); - Param migrated = paramCaptor.getValue(); - assertEquals(100L, migrated.getMonitorId()); - assertEquals("insecureSkipVerify", migrated.getField()); - assertEquals("true", migrated.getParamValue()); - } - - @Test - void leavesPinnedAndAlreadyMigratedSftpMonitorsUnchanged() throws Exception { - Monitor pinned = Monitor.builder().id(101L).app("ftp").build(); - Monitor migrated = Monitor.builder().id(102L).app("ftp").build(); - when(monitorDao.findMonitorsByAppEquals("ftp")).thenReturn(List.of(pinned, migrated)); - when(paramDao.findParamsByMonitorId(101L)).thenReturn(List.of( - Param.builder().monitorId(101L).field("ssl").paramValue("true").build(), - Param.builder().monitorId(101L).field("hostKeyFingerprint") - .paramValue("SHA256:pinned").build())); - when(paramDao.findParamsByMonitorId(102L)).thenReturn(List.of( - Param.builder().monitorId(102L).field("ssl").paramValue("true").build(), - Param.builder().monitorId(102L).field("insecureSkipVerify") - .paramValue("true").build())); - - migration.run(); - - verify(paramDao, never()).save(org.mockito.ArgumentMatchers.any()); - } -} diff --git a/home/docs/help/ftp.md b/home/docs/help/ftp.md index 13787e6ad4e..0d87457bebe 100644 --- a/home/docs/help/ftp.md +++ b/home/docs/help/ftp.md @@ -21,8 +21,8 @@ keywords: [ open source monitoring tool, open source ftp server monitoring tool, | 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 temporary unsafe compatibility is enabled. | -| Skip host key verification | **Dangerous temporary option.** It keeps compatibility but does not authenticate the SFTP server. | +| 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. | @@ -46,22 +46,9 @@ 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. -### Upgrade from an earlier release - -At the first startup after this change, existing SFTP monitors that have no -host-key setting are explicitly migrated to the temporary -`insecureSkipVerify` compatibility option so collection does not stop. HertzBeat -logs a warning for the migration and whenever such a monitor connects. - -For every migrated monitor: - -1. Verify the server fingerprint through a trusted channel. -2. Add it to **SFTP Host Key Fingerprints**. -3. Turn off **DANGER: Temporarily Skip SFTP Host Key Verification**. -4. Run detection and confirm collection succeeds. - -New SFTP monitors and imported configurations must pin at least one fingerprint -unless the operator explicitly selects the dangerous temporary option. +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 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 4da89fe373f..ef5f6c61b93 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 @@ -21,8 +21,8 @@ keywords: [ 开源监控系统, 开源FTP服务器监控工具, 监控FTP指标 | 用户名 | 连接FTP服务的用户名, 可选。 | | 密码 | 连接FTP服务的密码,可选。 | | 启用SFTP | 使用SFTP替代FTP;SFTP必须配置用户名和密码。 | -| SFTP主机密钥指纹 | 可信的SFTP服务器SHA-256指纹,每行一个或使用逗号分隔;除非显式启用临时不安全兼容模式,否则必填。 | -| 跳过主机密钥验证 | **危险的临时选项。** 可以保持兼容,但无法验证SFTP服务器身份。 | +| SFTP主机密钥指纹 | 可信的SFTP服务器SHA-256指纹,每行一个或使用逗号分隔;除非显式跳过验证,否则必填。 | +| 跳过主机密钥验证 | **危险选项。** 仅应用于受控诊断;启用后无法验证SFTP服务器身份。 | | 采集间隔 | 监控周期性采集数据间隔时间,单位秒,可设置的最小间隔为30秒。 | | 绑定标签 | 用于对监控资源进行分类管理。 | | 描述备注 | 更多标识和描述此监控的备注信息,用户可以在这里备注信息。 | @@ -43,21 +43,8 @@ ssh-keygen -lf /tmp/sftp-host-keys -E sha256 计划轮换主机密钥时,先通过可信渠道核对新密钥,将旧、新指纹同时加入配置,再轮换 服务器密钥;所有HertzBeat采集器都使用新密钥后,才能删除旧指纹。 -### 从旧版本升级 - -升级后首次启动时,如果已有SFTP监控没有主机密钥配置,HertzBeat会显式为其加入 -临时`insecureSkipVerify`兼容选项,避免采集中断。迁移时以及该监控每次连接时都会 -记录警告。 - -请逐个处理迁移后的监控: - -1. 通过可信渠道核对服务器指纹。 -2. 将指纹填写到“SFTP主机密钥指纹”。 -3. 关闭“危险:临时跳过SFTP主机密钥验证”。 -4. 执行探测并确认采集成功。 - -新建SFTP监控和导入的配置必须至少固定一个指纹;只有操作员显式选择危险的临时 -选项时才允许跳过验证。 +SFTP监控和导入的配置必须至少固定一个指纹;只有操作员显式选择危险的跳过验证 +选项时才允许省略。HertzBeat不会自动启用该选项。 ### 采集指标