AnTran BlogAnTran Blog
HomeCategoriesAbout
A
AnTran Blog
Home
Categories
About

Connect with me

GitHubTwitterLinkedIn
Home/Cloud & DevOps/Hướng dẫn A-Z: Bảo mật Strapi & Next.js với S3 Private, CloudFront và Domain riêng
Cloud & DevOps

Hướng dẫn A-Z: Bảo mật Strapi & Next.js với S3 Private, CloudFront và Domain riêng

An Tran
November 9, 2025
6 min read
Hướng dẫn A-Z: Bảo mật Strapi & Next.js với S3 Private, CloudFront và Domain riêng

🎯 Giới thiệu

Khi xây dựng một ứng dụng Next.js với Strapi làm CMS, vấn đề nan giải là quản lý media. Hầu hết các hướng dẫn trên mạng đều chỉ bạn cách "dễ nhất": set bucket S3 thành public-read. Đây là một rủi ro bảo mật lớn, cho phép bất kỳ ai truy cập và lạm dụng bucket của bạn.

Trong bài viết này, chúng ta sẽ làm theo cách chuyên nghiệp và bảo mật nhất:

  1. S3 Bucket sẽ được khóa Private 100%.
  2. CloudFront (CDN) sẽ là "cổng gác" duy nhất để truy cập file.
  3. Strapi sẽ upload file lên S3 và trả về URL CloudFront chính xác.
  4. Next.js sẽ hiển thị ảnh tối ưu từ CloudFront.
  5. Tất cả sẽ chạy qua domain của riêng bạn (ví dụ: media.yourdomain.com).

Ước tính chi phí: Gần như MIỄN PHÍ cho hàng triệu lượt truy cập nhờ vào gói Free Tier vĩnh viễn (1TB/tháng) của CloudFront.

Phần 1: 🏗️ Dựng nền móng AWS (S3, IAM, CloudFront)

1.1. Tạo S3 Bucket (Kho chứa riêng tư)

  1. Truy cập dịch vụ S3 trên AWS Console.
  2. Tạo một bucket mới (ví dụ: my-app-media).
  3. Quan trọng: Tại mục "Block Public Access settings", hãy check vào "Block all public access". Đây là chìa khóa của toàn bộ kiến trúc bảo mật này.

1.2. Tạo IAM User (Chìa khóa cho Strapi)

Strapi cần "chìa khóa" để có quyền upload file. Chúng ta sẽ tạo một user với quyền hạn tối thiểu.

  1. Truy cập dịch vụ IAM > Users > Create user.
  2. Đặt tên (ví dụ: strapi-s3-uploader).
  3. Chọn "Attach policies directly" > "Create policy".
  4. Chọn tab JSON và dán policy sau (thay TEN-BUCKET-CUA-BAN):
  5. Sau khi tạo user, vào tab "Security credentials" > "Create access key". Lưu lại Access key và Secret access key.
plaintext
1{
2  "Version": "2012-10-17",
3  "Statement": [
4    {
5      "Sid": "AllowStrapiToManageFiles",
6      "Effect": "Allow",
7      "Action": [
8        "s3:PutObject",
9        "s3:GetObject",
10        "s3:DeleteObject"
11      ],
12      "Resource": "arn:aws:s3:::TEN-BUCKET-CUA-BAN/*"
13    },
14    {
15      "Sid": "AllowStrapiToListBucket",
16      "Effect": "Allow",
17      "Action": "s3:ListBucket",
18      "Resource": "arn:aws:s3:::TEN-BUCKET-CUA-BAN"
19    }
20  ]
21}

Lưu ý: Chúng ta đã bỏ s3:PutObjectAcl vì chúng ta sẽ không dùng ACLs.

1.3. Tạo CloudFront (Cổng gác công khai)

  1. Truy cập dịch vụ CloudFront > Create distribution.
  2. Origin domain: Chọn S3 bucket của bạn.
  3. S3 bucket access:
    • Chọn "Yes use OAC (Origin Access Control)".
    • Nhấn "Create new OAC". CloudFront sẽ tự động tạo.
    • Nhấn nút Copy policy và Update S3 bucket policy (CloudFront sẽ tự động cập nhật policy của S3, cho phép CloudFront quyền GetObject).
  4. Viewer Protocol Policy: Chọn "Redirect HTTP to HTTPS".
  5. Tạo distribution và lưu lại Distribution domain name (ví dụ: d12345abcdef.cloudfront.net).

Phần 2: 🌐 Cấu hình Domain riêng (ví dụ: media.yourdomain.dev)

2.1. Yêu cầu SSL Certificate (ACM)

  1. Truy cập dịch vụ AWS Certificate Manager (ACM).
  2. Rất quan trọng: Chuyển Region sang us-east-1 (N. Virginia). CloudFront yêu cầu chứng chỉ phải ở region này.
  3. Yêu cầu một chứng chỉ public cho domain của bạn (ví dụ: media.yourdomain.dev).
  4. Chọn DNS validation. AWS sẽ cho bạn một bản ghi CNAME để thêm vào DNS.
  5. Thêm bản ghi CNAME này vào nhà quản lý DNS của bạn. Đợi đến khi trạng thái "Issued".

2.2. Trỏ Domain vào CloudFront

  1. Tại nhà quản lý DNS của bạn, tạo một bản ghi CNAME mới:
    • Host/Name: media
    • Value/Target: Trỏ vào domain CloudFront của bạn (d12345abcdef.cloudfront.net).

2.3. Cập nhật CloudFront

  1. Quay lại CloudFront, chọn distribution của bạn > "Edit".
  2. Alternate domain name (CNAME): Thêm domain của bạn (media.yourdomain.dev).
  3. Custom SSL certificate: Chọn chứng chỉ bạn vừa tạo ở ACM.
  4. Lưu lại và đợi deploy.

Phần 3: ✍️ Cấu hình Strapi (Tự động trả về URL CDN)

Đây là file config "vàng" cho Strapi để giải quyết mọi vấn đề (ACL, s3Options, URL).

3.1. Cài đặt Provider

plaintext
1npm install @strapi/provider-upload-aws-s3

3.2. Cấu hình plugins.js

Tạo file ./config/plugins.js với nội dung sau:

javascript
1// ./config/plugins.js
2module.exports = ({ env }) => ({
3  upload: {
4    config: {
5      provider: 'aws-s3',
6      providerOptions: {
7        // Cấu hình S3 SDK (quan trọng, phải nằm trong s3Options)
8        s3Options: {
9          accessKeyId: env('AWS_ACCESS_KEY_ID'),
10          secretAccessKey: env('AWS_SECRET_ACCESS_KEY'),
11          region: env('AWS_REGION'),
12        },
13        // Thông tin bucket
14        params: {
15          Bucket: env('AWS_BUCKET_NAME'),
16        },
17        // TỰ ĐỘNG trả về URL CloudFront
18        baseUrl: env('CDN_URL'),
19      },
20      actionOptions: {
21        // Tắt ACL để tương thích với S3 bảo mật mới
22        upload: {
23          ACL: null
24        },
25        uploadStream: {
26          ACL: null
27        },
28        delete: {},
29      },
30    },
31  },
32});

3.3. Cấu hình .env

Thêm các biến sau vào file .env của Strapi:

plaintext
1AWS_ACCESS_KEY_ID=KEY_CUA_BAN
2AWS_SECRET_ACCESS_KEY=SECRET_CUA_BAN
3AWS_REGION=ap-southeast-1
4AWS_BUCKET_NAME=my-app-media
5CDN_URL=https://media.yourdomain.dev

Khởi động lại Strapi. Giờ đây, khi bạn upload file, Strapi sẽ tự động lưu URL CDN vào database!

Phần 4: 🖼️ Cấu hình Next.js (Hiển thị ảnh tối ưu)

Vì Strapi đã trả về URL chính xác, phần Next.js trở nên vô cùng đơn giản.

4.1. Cấu hình next.config.js

Chúng ta cần "whitelist" domain CDN để Next.js <Image> có thể tối ưu nó.

javascript
1// next.config.js
2import type { NextConfig } from 'next';
3
4const nextConfig: NextConfig = {
5    images: {
6        remotePatterns: [
7            // Thêm pattern cho domain CDN của bạn
8            {
9                protocol: 'https',
10                hostname: 'media.yourdomain.dev',
11                port: '',
12                pathname: '/**',
13            },
14        ],
15    },
16};
17
18export default nextConfig;

Khởi động lại server Next.js.

4.2. Sử dụng trong Component

Không cần hàm replace() hay helper. Bạn chỉ cần dùng thẳng URL từ API.

javascript
1import Image from 'next/image';
2
3function MyPost({ post }) {
4  // URL này đã là "https://media.yourdomain.dev/file.jpg"
5  const imageUrl = post.attributes.image.data.attributes.url;
6
7  return (
8    <Image
9      src={imageUrl}
10      alt="Mô tả ảnh"
11      width={500}
12      height={300}
13    />
14  );
15}

Phần 5: 🚨 Xử lý lỗi "Gotchas" (CORS & CSP)

Sau khi làm các bước trên, bạn sẽ thấy ảnh hiển thị trên Next.js, nhưng bị lỗi trong Strapi Admin. Đây là lý do:

5.1. Lỗi CORS (CloudFront chặn Strapi Admin)

  • Vấn đề: Trình duyệt (chạy Strapi Admin trên localhost:1337) cố tải ảnh từ media.yourdomain.dev, nhưng CloudFront không cho phép.
  • Giải pháp: Tạo Response Headers Policy trên CloudFront.
    1. Vào CloudFront > Policies > Response headers > Create.
    2. Đặt tên (ví dụ: Strapi-Admin-CORS-Policy).
    3. Bật CORS.
    4. Access-Control-Allow-Origins: Thêm 2 dòng: http://localhost:1337 và domain Strapi production của bạn (ví dụ: https://admin.yourdomain.dev).
    5. Access-Control-Allow-Methods: Chọn ALL.
    6. Access-Control-Allow-Headers: Chọn *.
    7. Gắn policy này vào CloudFront Distribution của bạn (trong tab Behaviors > Edit).

5.2. Lỗi CSP (Strapi Admin tự chặn chính mình)

  • Vấn đề: Strapi có một lớp bảo mật (Content Security Policy) nói rằng "Tôi chỉ tin ảnh từ self, data:, ...". Nó không tin domain media.yourdomain.dev của bạn.
  • Giải pháp: Cập nhật file ./config/middlewares.js của Strapi.
javascript
1// ./config/middlewares.js
2module.exports = [
3  'strapi::errors',
4  {
5    name: 'strapi::security',
6    config: {
7      contentSecurityPolicy: {
8        useDefaults: true,
9        directives: {
10          'connect-src': ["'self'", 'https:'],
11          'img-src': [
12            "'self'",
13            'data:',
14            'blob:',
15            'https://media.yourdomain.dev', // <-- THÊM DOMAIN CỦA BẠN
16          ],
17          'media-src': [
18            "'self'",
19            'data:',
20            'blob:',
21            'https://media.yourdomain.dev', // <-- THÊM DOMAIN CỦA BẠN
22          ],
23        },
24      },
25    },
26  },
27  'strapi::cors',
28  'strapi::poweredBy',
29  // ... các middleware khác
30  'strapi::public',
31];

Khởi động lại Strapi và bạn sẽ thấy ảnh hiển thị hoàn hảo ở mọi nơi.

Phần 6: 🔒 Lời cuối: "Nếu Access Key bị lộ thì sao?"

Bạn phải hiểu rõ: CORS và CSP là cơ chế bảo mật trình duyệt. Chúng KHÔNG bảo vệ bạn nếu Access Key bị lộ.

Nếu AWS_SECRET_ACCESS_KEY của bạn bị lộ, kẻ tấn công có thể dùng nó từ server của họ (phớt lờ CORS) để xóa sạch toàn bộ S3 bucket của bạn.

Trách nhiệm bảo mật lớn nhất của bạn là:

  1. Tuyệt đối không commit file .env lên Git (luôn thêm .env vào .gitignore).
  2. Sử dụng các dịch vụ quản lý biến môi trường (như Vercel, Netlify, AWS Secrets Manager) khi deploy.
  3. (Nâng cao): Giới hạn IAM Policy chỉ chấp nhận yêu cầu từ địa chỉ IP tĩnh của server Strapi.

Chúc mừng! Bạn đã xây dựng một hệ thống media mạnh mẽ, bảo mật, và chi phí cực thấp.

Share:
An Tran

An Tran

Software Engineer, sharing my passion for creating beautiful web experiences.

GitHubLinkedInWebsite
Tags:#AWS#S3#Strapi#Next.js#Headless CMS#Tutorial
AnTran BlogAnTran Blog

Quick Links

  • Home
  • About
  • Categories
  • Portfolio

Categories

  • Product Development
  • Cloud & DevOps

Connect

GitHubLinkedIn

© 2025 AnTran Blog. All rights reserved.