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:
@@ -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)
|
||||||
|
|||||||
8
.github/workflows/e2e.yml
vendored
8
.github/workflows/e2e.yml
vendored
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
29
pnpm-lock.yaml
generated
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user