fix(security): remove MinIO hardcoded credentials & add presigned URL support

- Remove hardcoded minioadmin/minioadmin_secret fallback from docker-compose.yml,
  require MINIO_ACCESS_KEY/MINIO_SECRET_KEY env vars (fail-fast with :? syntax)
- Align docker-compose.yml env var names with .env.example (MINIO_ACCESS_KEY/SECRET_KEY)
- Update CI e2e workflow to use GitHub vars with non-default fallbacks
- Update .env.test to use non-default test credentials
- Add @aws-sdk/s3-request-presigner and getPresignedUploadUrl() method to
  MinioMediaStorageService for properly signed client-side uploads
- Remove hardcoded credentials from dev-environment docs

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Ho Ngoc Hai
2026-04-08 22:44:50 +07:00
parent 8a86cf42d4
commit 03231271ca
7 changed files with 50 additions and 9 deletions

View File

@@ -16,8 +16,8 @@ TYPESENSE_API_KEY=ts_dev_key_change_me
# MinIO # MinIO
MINIO_ENDPOINT=localhost MINIO_ENDPOINT=localhost
MINIO_PORT=9000 MINIO_PORT=9000
MINIO_ACCESS_KEY=minioadmin MINIO_ACCESS_KEY=test_minio_user
MINIO_SECRET_KEY=minioadmin_secret MINIO_SECRET_KEY=test_minio_secret_key_32chars!!
MINIO_BUCKET=goodgo-uploads MINIO_BUCKET=goodgo-uploads
# Auth (deterministic secrets for test reproducibility) # Auth (deterministic secrets for test reproducibility)

View File

@@ -60,8 +60,8 @@ jobs:
ports: ports:
- 9000:9000 - 9000:9000
env: env:
MINIO_ROOT_USER: minioadmin MINIO_ROOT_USER: ${{ vars.CI_MINIO_ACCESS_KEY || 'ci_minio_user' }}
MINIO_ROOT_PASSWORD: minioadmin_secret MINIO_ROOT_PASSWORD: ${{ vars.CI_MINIO_SECRET_KEY || 'ci_minio_secret_key_32chars!!' }}
options: >- options: >-
--health-cmd "curl -sf http://localhost:9000/minio/health/live || exit 1" --health-cmd "curl -sf http://localhost:9000/minio/health/live || exit 1"
--health-interval 10s --health-interval 10s
@@ -77,8 +77,8 @@ jobs:
TYPESENSE_API_KEY: ts_ci_key TYPESENSE_API_KEY: ts_ci_key
MINIO_ENDPOINT: localhost MINIO_ENDPOINT: localhost
MINIO_PORT: 9000 MINIO_PORT: 9000
MINIO_ACCESS_KEY: minioadmin MINIO_ACCESS_KEY: ${{ vars.CI_MINIO_ACCESS_KEY || 'ci_minio_user' }}
MINIO_SECRET_KEY: minioadmin_secret MINIO_SECRET_KEY: ${{ vars.CI_MINIO_SECRET_KEY || 'ci_minio_secret_key_32chars!!' }}
MINIO_BUCKET: goodgo-uploads MINIO_BUCKET: goodgo-uploads
NODE_ENV: test NODE_ENV: test
JWT_SECRET: e2e-test-jwt-secret-key JWT_SECRET: e2e-test-jwt-secret-key

View File

@@ -14,6 +14,7 @@
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.1026.0", "@aws-sdk/client-s3": "^3.1026.0",
"@aws-sdk/s3-request-presigner": "^3.1026.0",
"@goodgo/mcp-servers": "workspace:*", "@goodgo/mcp-servers": "workspace:*",
"@nestjs/common": "^11.0.0", "@nestjs/common": "^11.0.0",
"@nestjs/core": "^11.0.0", "@nestjs/core": "^11.0.0",

View File

@@ -7,6 +7,7 @@ import {
HeadBucketCommand, HeadBucketCommand,
CreateBucketCommand, CreateBucketCommand,
} from '@aws-sdk/client-s3'; } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { Injectable, type OnModuleInit } from '@nestjs/common'; import { Injectable, type OnModuleInit } from '@nestjs/common';
import { type LoggerService } from '@modules/shared/infrastructure/logger.service'; import { type LoggerService } from '@modules/shared/infrastructure/logger.service';
@@ -15,6 +16,7 @@ export const MEDIA_STORAGE_SERVICE = Symbol('MEDIA_STORAGE_SERVICE');
export interface IMediaStorageService { export interface IMediaStorageService {
upload(buffer: Buffer, originalName: string, mimeType: string, folder: string): Promise<string>; upload(buffer: Buffer, originalName: string, mimeType: string, folder: string): Promise<string>;
delete(fileUrl: string): Promise<void>; delete(fileUrl: string): Promise<void>;
getPresignedUploadUrl(objectKey: string, mimeType: string, expiresInSeconds?: number): Promise<string>;
} }
function requireEnv(key: string): string { function requireEnv(key: string): string {
@@ -106,6 +108,15 @@ export class MinioMediaStorageService implements IMediaStorageService, OnModuleI
} }
} }
async getPresignedUploadUrl(objectKey: string, mimeType: string, expiresInSeconds = 300): Promise<string> {
const command = new PutObjectCommand({
Bucket: this.bucket,
Key: objectKey,
ContentType: mimeType,
});
return getSignedUrl(this.s3, command, { expiresIn: expiresInSeconds });
}
async delete(fileUrl: string): Promise<void> { async delete(fileUrl: string): Promise<void> {
try { try {
const urlObj = new URL(fileUrl); const urlObj = new URL(fileUrl);

View File

@@ -68,8 +68,8 @@ services:
- '${MINIO_CONSOLE_PORT:-9001}:9001' - '${MINIO_CONSOLE_PORT:-9001}:9001'
command: server /data --console-address ":9001" command: server /data --console-address ":9001"
environment: environment:
MINIO_ROOT_USER: ${MINIO_USER:-minioadmin} MINIO_ROOT_USER: ${MINIO_ACCESS_KEY:?MINIO_ACCESS_KEY is required}
MINIO_ROOT_PASSWORD: ${MINIO_PASSWORD:-minioadmin_secret} MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY:?MINIO_SECRET_KEY is required}
volumes: volumes:
- minio_data:/data - minio_data:/data
healthcheck: healthcheck:

View File

@@ -130,7 +130,7 @@ curl http://localhost:8108/health
### MinIO ### MinIO
- **API**: `http://localhost:9000` - **API**: `http://localhost:9000`
- **Console**: `http://localhost:9001` (login: `minioadmin` / `minioadmin_secret`) - **Console**: `http://localhost:9001` (login with `MINIO_ACCESS_KEY` / `MINIO_SECRET_KEY` from your `.env`)
### AI Services ### AI Services

29
pnpm-lock.yaml generated
View File

@@ -72,6 +72,9 @@ importers:
'@aws-sdk/client-s3': '@aws-sdk/client-s3':
specifier: ^3.1026.0 specifier: ^3.1026.0
version: 3.1026.0 version: 3.1026.0
'@aws-sdk/s3-request-presigner':
specifier: ^3.1026.0
version: 3.1026.0
'@goodgo/mcp-servers': '@goodgo/mcp-servers':
specifier: workspace:* specifier: workspace:*
version: link:../../libs/mcp-servers version: link:../../libs/mcp-servers
@@ -485,6 +488,10 @@ packages:
resolution: {integrity: sha512-6Q8B1dcx6BBqUTY1Mc/eROKA0FImEEY5VPSd6AGPEUf0ErjExz4snVqa9kNJSoVDV1rKaNf3qrWojgcKW+SdDg==} resolution: {integrity: sha512-6Q8B1dcx6BBqUTY1Mc/eROKA0FImEEY5VPSd6AGPEUf0ErjExz4snVqa9kNJSoVDV1rKaNf3qrWojgcKW+SdDg==}
engines: {node: '>=20.0.0'} engines: {node: '>=20.0.0'}
'@aws-sdk/s3-request-presigner@3.1026.0':
resolution: {integrity: sha512-PBVt/zb4YsJMcyB/HbGmID4RP00dTkdQGkNQiw1i6oXQ/U8hnPEI8+IvTKR4+5YEQ8Cq4QmtIV0mzv070L+oOg==}
engines: {node: '>=20.0.0'}
'@aws-sdk/signature-v4-multi-region@3.996.16': '@aws-sdk/signature-v4-multi-region@3.996.16':
resolution: {integrity: sha512-EMdXYB4r/k5RWq86fugjRhid5JA+Z6MpS7n4sij4u5/C+STrkvuf9aFu41rJA9MjUzxCLzv8U2XL8cH2GSRYpQ==} resolution: {integrity: sha512-EMdXYB4r/k5RWq86fugjRhid5JA+Z6MpS7n4sij4u5/C+STrkvuf9aFu41rJA9MjUzxCLzv8U2XL8cH2GSRYpQ==}
engines: {node: '>=20.0.0'} engines: {node: '>=20.0.0'}
@@ -505,6 +512,10 @@ packages:
resolution: {integrity: sha512-2nUQ+2ih7CShuKHpGSIYvvAIOHy52dOZguYG36zptBukhw6iFwcvGfG0tes0oZFWQqEWvgZe9HLWaNlvXGdOrg==} resolution: {integrity: sha512-2nUQ+2ih7CShuKHpGSIYvvAIOHy52dOZguYG36zptBukhw6iFwcvGfG0tes0oZFWQqEWvgZe9HLWaNlvXGdOrg==}
engines: {node: '>=20.0.0'} engines: {node: '>=20.0.0'}
'@aws-sdk/util-format-url@3.972.9':
resolution: {integrity: sha512-fNJXHrs0ZT7Wx0KGIqKv7zLxlDXt2vqjx9z6oKUQFmpE5o4xxnSryvVHfHpIifYHWKz94hFccIldJ0YSZjlCBw==}
engines: {node: '>=20.0.0'}
'@aws-sdk/util-locate-window@3.965.5': '@aws-sdk/util-locate-window@3.965.5':
resolution: {integrity: sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==} resolution: {integrity: sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==}
engines: {node: '>=20.0.0'} engines: {node: '>=20.0.0'}
@@ -6273,6 +6284,17 @@ snapshots:
'@smithy/types': 4.14.0 '@smithy/types': 4.14.0
tslib: 2.8.1 tslib: 2.8.1
'@aws-sdk/s3-request-presigner@3.1026.0':
dependencies:
'@aws-sdk/signature-v4-multi-region': 3.996.16
'@aws-sdk/types': 3.973.7
'@aws-sdk/util-format-url': 3.972.9
'@smithy/middleware-endpoint': 4.4.29
'@smithy/protocol-http': 5.3.13
'@smithy/smithy-client': 4.12.9
'@smithy/types': 4.14.0
tslib: 2.8.1
'@aws-sdk/signature-v4-multi-region@3.996.16': '@aws-sdk/signature-v4-multi-region@3.996.16':
dependencies: dependencies:
'@aws-sdk/middleware-sdk-s3': 3.972.28 '@aws-sdk/middleware-sdk-s3': 3.972.28
@@ -6311,6 +6333,13 @@ snapshots:
'@smithy/util-endpoints': 3.3.4 '@smithy/util-endpoints': 3.3.4
tslib: 2.8.1 tslib: 2.8.1
'@aws-sdk/util-format-url@3.972.9':
dependencies:
'@aws-sdk/types': 3.973.7
'@smithy/querystring-builder': 4.2.13
'@smithy/types': 4.14.0
tslib: 2.8.1
'@aws-sdk/util-locate-window@3.965.5': '@aws-sdk/util-locate-window@3.965.5':
dependencies: dependencies:
tslib: 2.8.1 tslib: 2.8.1