[fix] stop exposing the jwt and aes master keys over the config api (#4270)

This commit is contained in:
Duansg
2026-08-03 11:30:56 +08:00
committed by GitHub
parent 16d420a245
commit b1a4f9630e
10 changed files with 202 additions and 0 deletions
@@ -23,11 +23,14 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.constraints.NotNull;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.constants.GeneralConfigTypeEnum;
import org.apache.hertzbeat.common.entity.dto.Message;
import org.apache.hertzbeat.common.util.CommonUtil;
import org.apache.hertzbeat.common.util.ResponseUtil;
import org.apache.hertzbeat.manager.pojo.dto.TemplateConfig;
import org.apache.hertzbeat.manager.service.ConfigService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@@ -61,6 +64,18 @@ public class GeneralConfigController {
private static final Set<String> ZONE_IDS = ZoneId.getAvailableZoneIds();
/**
* Config types that must never travel over the rest api.
*
* <p>The {@code secret} config holds the jwt signing key and the aes key that protects
* every stored monitor credential. Both are loaded straight from the persistence layer
* while the application boots and no part of the ui reads them, so handing them out
* over http has no legitimate use and would let a reader mint admin tokens and decrypt
* stored credentials. Refusing the read here keeps the guarantee even if the rbac rules
* for this route are ever loosened again.
*/
private static final Set<String> NON_READABLE_TYPES = Set.of(GeneralConfigTypeEnum.secret.name());
@Resource
private ConfigService configService;
@@ -80,6 +95,12 @@ public class GeneralConfigController {
public ResponseEntity<Message<Object>> getConfig(
@Parameter(description = "Config Type", example = "email")
@PathVariable("type") @NotNull final String type) {
if (NON_READABLE_TYPES.contains(type)) {
log.warn("Refused to serve the {} config over the rest api", type);
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(Message.fail(CommonConstants.FAIL_CODE,
"The " + type + " config can not be read through the rest api."));
}
return ResponseUtil.handle(() -> configService.getConfig(type));
}
@@ -20,6 +20,8 @@ package org.apache.hertzbeat.manager.controller;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
@@ -84,6 +86,18 @@ class GeneralConfigControllerTest {
.andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE));
}
@Test
public void testGetSecretConfigIsRefused() throws Exception {
mockMvc.perform(get("/api/config/secret")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isForbidden())
.andExpect(jsonPath("$.code").value((int) CommonConstants.FAIL_CODE));
// the jwt signing key and the aes key must not even be loaded for a rest read
verify(configService, never()).getConfig(anyString());
}
@Test
public void testUpdateTemplateAppConfig() throws Exception {