refactor(center): local-only guild register endpoint without shared secret

This commit is contained in:
nav
2026-05-13 08:41:45 +00:00
parent 1c07f43032
commit 0a4cb62065
3 changed files with 11 additions and 30 deletions

View File

@@ -15,6 +15,3 @@ JWT_ACCESS_SECRET=change-me-access
JWT_REFRESH_SECRET=change-me-refresh JWT_REFRESH_SECRET=change-me-refresh
JWT_ACCESS_EXPIRES_IN=15m JWT_ACCESS_EXPIRES_IN=15m
JWT_REFRESH_EXPIRES_IN=30d JWT_REFRESH_EXPIRES_IN=30d
# Center <-> Guild handshake
CENTER_SHARED_SECRET=change-me-center-secret

View File

@@ -20,7 +20,6 @@ function validateEnv(): void {
requireEnv('DB_USER'); requireEnv('DB_USER');
requireEnv('DB_PASSWORD'); requireEnv('DB_PASSWORD');
requireEnv('DB_NAME'); requireEnv('DB_NAME');
requireEnv('CENTER_SHARED_SECRET');
requireEnv('JWT_ACCESS_SECRET'); requireEnv('JWT_ACCESS_SECRET');
requireEnv('JWT_REFRESH_SECRET'); requireEnv('JWT_REFRESH_SECRET');
} }

View File

@@ -13,6 +13,7 @@ import {
Patch, Patch,
Post, Post,
Query, Query,
Req,
} from '@nestjs/common'; } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
@@ -22,12 +23,6 @@ import { GuildNode } from '../entities/guild-node.entity';
import { AuditService } from '../audit/audit.service'; import { AuditService } from '../audit/audit.service';
import { RegisterNodeDto } from './dto.register-node.dto'; import { RegisterNodeDto } from './dto.register-node.dto';
import { UpdateNodeStatusDto } from './dto.update-node-status.dto'; import { UpdateNodeStatusDto } from './dto.update-node-status.dto';
import {
buildCanonical,
safeEqualHex,
signCanonical,
verifyRequestTime,
} from '../common/hmac';
import { FABRIC_PROTOCOL_VERSION, normalizeVersion } from '../common/version'; import { FABRIC_PROTOCOL_VERSION, normalizeVersion } from '../common/version';
@Controller('nodes') @Controller('nodes')
@@ -40,12 +35,19 @@ export class NodesController {
@Post('register') @Post('register')
async register( async register(
@Req() req: { ip?: string; socket?: { remoteAddress?: string } },
@Body() body: RegisterNodeDto, @Body() body: RegisterNodeDto,
@Headers('x-fabric-signature') signature?: string,
@Headers('x-fabric-timestamp') timestamp?: string,
@Headers('x-fabric-nonce') nonce?: string,
@Headers('x-fabric-version') fabricVersion?: string, @Headers('x-fabric-version') fabricVersion?: string,
) { ) {
const remoteAddress = (req.ip ?? req.socket?.remoteAddress ?? '').toLowerCase();
const isLoopback =
remoteAddress === '127.0.0.1' ||
remoteAddress === '::1' ||
remoteAddress === '::ffff:127.0.0.1';
if (!isLoopback) {
throw new ForbiddenException('register endpoint only allows localhost caller');
}
const requestedVersion = normalizeVersion(fabricVersion); const requestedVersion = normalizeVersion(fabricVersion);
if (requestedVersion !== FABRIC_PROTOCOL_VERSION) { if (requestedVersion !== FABRIC_PROTOCOL_VERSION) {
throw new HttpException( throw new HttpException(
@@ -61,23 +63,6 @@ export class NodesController {
); );
} }
const secret = process.env.CENTER_SHARED_SECRET as string;
if (!signature || !timestamp || !nonce || !verifyRequestTime(timestamp)) {
throw new ForbiddenException('invalid hmac headers');
}
const canonical = buildCanonical({
method: 'POST',
path: '/api/nodes/register',
timestamp,
nonce,
body: JSON.stringify(body),
});
const expected = signCanonical(secret, canonical);
if (!safeEqualHex(signature, expected)) {
throw new ForbiddenException('invalid shared secret');
}
const existedByNodeId = await this.nodeRepo.findOne({ const existedByNodeId = await this.nodeRepo.findOne({
where: { nodeId: body.nodeId }, where: { nodeId: body.nodeId },
}); });