ข้ามไปยังเนื้อหา

Security and Atlas

mongod ใหม่ที่สตาร์ทด้วยค่าเริ่มต้นไม่มี authentication บนแล็ปท็อปของคุณที่ bind กับ localhost ไม่เป็นอันตราย แต่บน server ที่มี public IP มันคือการรั่วไหลที่รอเกิด — และ MongoDB ที่ไม่มีการป้องกันทำข้อมูลจำนวนมหาศาลรั่วมาแล้วด้วยวิธีนี้ ความปลอดภัยใน production คือชั้นป้องกันสั้น ๆ ที่แต่ละชั้นปิดประตูคนละบาน: การเชื่อมต่อมาจากที่ไหนได้, เชื่อมต่ออย่างไรให้ปลอดภัย, คุณเป็นใคร, ทำอะไรได้ และอะไรอยู่บนดิสก์

ทุกอย่างที่ระบุ lab รันบนเครื่อง Mac นี้ บน container mongo:8.3.11 ชั่วคราว (mongodb-b3-sec จำกัด 512 MB ลบทิ้งหลังใช้; CA และ server certificate ชั่วคราวที่สร้างด้วย openssl, keyfile แบบสุ่ม; ไม่มีอะไรออกจากเครื่องและไม่เกี่ยวข้องกับบัญชีคลาวด์) ตัวตรวจอัตโนมัติของคอร์สรัน fence เหล่านี้ไม่ได้ (ต้องมี server แบบ --auth หรือ TLS) output จึงบันทึกจากการรันจริง รหัสผ่านในบทนี้เป็น placeholder เช่น <your-password> — อย่าวางของจริงลงใน snippet แท็บ Python/Go/Rust สะท้อนการเรียกของ Node ที่ตรวจแล้วและไม่ได้ถูกรันโดยตัวตรวจ

flowchart LR
  Client["Application"] --> Net["Network gate — firewall, bind IP, VPC"]
  Net --> TLS["TLS — encrypts the connection"]
  TLS --> Auth["Authentication — who are you"]
  Auth --> RBAC["Authorization — what may you do"]
  RBAC --> Data["Data — encrypted at rest on disk"]
แต่ละประตูปิดช่องทางต่างกัน: เครือข่าย จากนั้นการเข้ารหัสระหว่างส่ง แล้วตัวตน แล้วสิทธิ์ แล้วการเข้ารหัสข้อมูลที่เก็บ

ไม่มีชั้นเดียวที่เพียงพอ authentication ที่ไม่มีการจำกัดเครือข่ายยังเชิญ brute force; เครือข่ายที่ล็อกแน่นแต่ไม่มี auth เชื่อใจทุกคนภายใน

container mongo:8.3.11 ธรรมดา ไม่มี flag (publish บน loopback ของ host เท่านั้น):

scripts/sec-stage1.sh
docker run -d --name mongo-open --memory 512m -p 127.0.0.1:27304:27017 mongo:8 mongod --wiredTigerCacheSizeGB 0.25
docker exec mongo-open mongosh --quiet --file /tmp/open.js # the script below
// mongosh/sec-open.js @skip-verify lab: needs a plain throwaway mongod (real output below)
console.log(db.adminCommand({ listDatabases: 1 }).databases.map((d) => d.name));
db.getSiblingDB("library").members.insertOne({ name: "anyone" });
console.log("insert with no credentials: ok");
console.log(db.serverCmdLineOpts().argv);
[ 'admin', 'config', 'local' ]
insert with no credentials: ok
[ 'mongod', '--wiredTigerCacheSizeGB', '0.25', '--bind_ip_all' ]

ไม่มี credential แต่แสดงรายการ database และเขียนข้อมูลได้ สังเกตบรรทัดสุดท้าย: image ทางการสตาร์ท mongod ด้วย --bind_ip_all ซึ่งต่างจาก binary เปล่าที่ตาม manual “bind[s] to localhost by default” (MongoDB configuration hardening, ตรวจเมื่อ 2026-09-30) container ที่เข้าถึงได้ผ่าน port ที่ publish จึงเข้าถึงได้โดยทุกคนที่ไปถึง port นั้น: publish บน 127.0.0.1 เท่านั้นดังข้างบน

Authentication พิสูจน์ว่าคุณเป็นใคร; role-based access control จำกัดสิ่งที่คุณทำได้ หลักการคือ least privilege image สร้าง user แรก (root) จาก environment variable ให้ได้ บน data directory ที่ว่างเท่านั้น และสตาร์ทด้วย --auth:

scripts/sec-stage2.sh
docker run -d --name mongo-auth --memory 512m -p 127.0.0.1:27304:27017 \
-e MONGO_INITDB_ROOT_USERNAME=root -e MONGO_INITDB_ROOT_PASSWORD='<your-root-password>' \
mongo:8 mongod --auth --wiredTigerCacheSizeGB 0.25

ทีนี้ client แบบไม่ระบุตัวตน ping ยังใช้ได้ ที่เหลือถูกปฏิเสธด้วยข้อความที่ผู้ดูแลระบบทุกคนจำได้:

// mongosh/sec-anonymous.js @skip-verify lab: needs the --auth container (real output below)
console.log(db.runCommand({ ping: 1 }));
for (const op of [() => db.getSiblingDB("library").members.find().toArray(), () => db.adminCommand({ listDatabases: 1 })]) {
try { op(); } catch (e) { console.log(e.name, e.codeName, e.code, "-", e.message); }
}
{ ok: 1 }
MongoServerError Unauthorized 13 - Command find requires authentication
MongoServerError Unauthorized 13 - Command listDatabases requires authentication

สร้าง user แบบ least-privilege (ในฐานะ root; รหัสผ่านมาจาก environment variable — passwordPrompt() ใช้ได้เฉพาะใน shell แบบ interactive จึงต้องไม่ปรากฏในสคริปต์) แล้วดูว่า user แบบอ่านอย่างเดียวทำอะไรไม่ได้:

// mongosh/sec-users.js @skip-verify lab: run as root against the --auth container (real output below)
const admin = db.getSiblingDB("admin");
admin.createUser({ user: "library_app", pwd: process.env.APP_PW, roles: [{ role: "readWrite", db: "library" }] });
admin.createUser({ user: "library_reader", pwd: process.env.APP_PW, roles: [{ role: "read", db: "library" }] });
console.log(admin.getUsers().users.filter((u) => u.user.startsWith("library_")).map((u) => ({ user: u.user, roles: u.roles })));
const app = new Mongo("mongodb://library_app:" + process.env.APP_PW + "@localhost:27017/library?authSource=admin");
app.getDB("library").members.insertOne({ name: "Ada" });
const reader = new Mongo("mongodb://library_reader:" + process.env.APP_PW + "@localhost:27017/library?authSource=admin");
console.log("reader can read:", reader.getDB("library").members.countDocuments());
try { reader.getDB("library").members.insertOne({ name: "x" }); } catch (e) { console.log(e.codeName, e.code, "-", e.message); }
[
{
user: 'library_app',
roles: [ { role: 'readWrite', db: 'library' } ]
},
{
user: 'library_reader',
roles: [ { role: 'read', db: 'library' } ]
}
]
reader can read: 1
Unauthorized 13 - not authorized on library to execute command { insert: "members", documents: [ { name: "x", _id: ObjectId('6abcd5da1041bb9cb750d769') } ], ordered: true, lsid: { id: UUID("8927124f-ce75-486b-b03c-c18d8c4052ed") }, $db: "library" }

driver ส่ง credential ใน connection string database ใน path ของ URI ไม่ใช่ที่ที่ user อยู่: หากไม่ระบุ authSource driver จะ authenticate กับ database ที่ระบุใน path และ user เหล่านี้อยู่ใน admin

scripts/sec-connect.sh
mongosh "mongodb://library_app@localhost:27017/library?authSource=admin" --password "$APP_PW"

อีกสามชิ้น รวมกันในไฟล์ config เดียวและสตาร์ทจริง TLS เข้ารหัสการเชื่อมต่อ (และเมื่อมี CA file ให้ client ตรวจสอบ server ได้); keyfile คือความลับร่วมที่สมาชิก replica set ใช้ยืนยันตัวตนซึ่งกันและกัน (และเปิด authorization ด้วย); net.bindIp ตัดสินว่า mongod ฟังบน interface ไหน certificate authority ชั่วคราว, server certificate (ใช้ได้กับ localhost และ 127.0.0.1) และ keyfile สร้างแบบนี้ — ตัวไฟล์ไม่เคยลงใน repository:

scripts/sec-certs.sh
openssl req -x509 -newkey rsa:2048 -nodes -keyout ca.key -out ca.pem -days 2 -subj "/CN=lab-ca"
openssl req -newkey rsa:2048 -nodes -keyout server.key -out server.csr -subj "/CN=localhost"
printf "subjectAltName=DNS:localhost,IP:127.0.0.1" > san.ext
openssl x509 -req -in server.csr -CA ca.pem -CAkey ca.key -CAcreateserial -out server.crt -days 2 -extfile san.ext
cat server.key server.crt > server.pem
openssl rand -base64 756 > keyfile
# mongod.conf
security:
authorization: enabled
keyFile: /etc/mongo-lab/keyfile
net:
port: 27017
bindIp: 0.0.0.0
tls:
mode: requireTLS
certificateKeyFile: /etc/mongo-lab/server.pem
CAFile: /etc/mongo-lab/ca.pem
allowConnectionsWithoutCertificates: true
replication:
replSetName: rs0
storage:
wiredTiger:
engineConfig:
cacheSizeGB: 0.25

allowConnectionsWithoutCertificates: true สำคัญ: เมื่อกำหนด CAFile แล้ว mongod จะขอ certificate จาก client ด้วย (mutual TLS); ถ้าไม่มีบรรทัดนี้ client ที่ไม่มี certificate จะถูกตัดการเชื่อมต่อ และใน lab มันหน้าตาเหมือนความล้มเหลวของ plain-TCP ด้านล่างเป๊ะ (connection ... closed) bindIp: 0.0.0.0 ถูกต้องเฉพาะ ภายใน container ที่ port ที่ publish (loopback บน host) คือประตูจริง; บน host คุณควรระบุ address ส่วนตัว ไม่ใช่ 0.0.0.0 บน interface สาธารณะ keyfile ต้องเป็นของผู้ใช้ที่ mongod รันด้วย โดยไม่มีสิทธิ์ของ group/other การสตาร์ทด้วย keyfile ที่ใครก็อ่านได้ล้มทันที (บรรทัด log จริง):

scripts/sec-keyfile-perms.sh
docker run --rm --entrypoint bash -v "$PWD/certs:/certs:ro" mongo:8 -c \
'mkdir /etc/mongo-lab && cp /certs/* /etc/mongo-lab/ && chmod 444 /etc/mongo-lab/keyfile && exec gosu mongodb mongod --config /etc/mongo-lab/mongod.conf'
{"c":"ACCESS","id":20254,"msg":"Read security file failed","attr":{"error":{"code":30,"codeName":"InvalidPath","errmsg":"permissions on /etc/mongo-lab/keyfile are too open"}}}
{"s":"F","c":"CONTROL","id":20575,"msg":"Error creating service context","attr":{"error":"Location5579201: Unable to acquire security key[s]"}}

เมื่อ permission ถูกต้อง (chown mongodb, chmod 400) server สตาร์ทได้ user แรกถูกสร้างผ่าน localhost exception หลัง rs.initiate():

scripts/sec-stage3.sh
docker run -d --name mongo-secure --hostname sec-host --memory 512m -p 127.0.0.1:27305:27017 \
--entrypoint bash -v "$PWD/certs:/certs:ro" mongo:8 -c \
'mkdir /etc/mongo-lab && cp /certs/* /etc/mongo-lab/ && chown -R mongodb:mongodb /etc/mongo-lab && chmod 400 /etc/mongo-lab/* && exec gosu mongodb mongod --config /etc/mongo-lab/mongod.conf'
URI="mongodb://localhost/?tls=true&tlsCAFile=/etc/mongo-lab/ca.pem&directConnection=true"
docker exec mongo-secure mongosh --quiet "$URI" --eval 'rs.initiate({ _id: "rs0", members: [{ _id: 0, host: "localhost:27017" }] })'
docker exec -e ROOT_PW='<your-root-password>' mongo-secure mongosh --quiet "$URI" --eval 'db.getSiblingDB("admin").createUser({ user: "root", pwd: process.env.ROOT_PW, roles: ["root"] })'

จากนั้นสิ่งที่คุณจะเจอใน production ตามลำดับ: (1) TCP ธรรมดากับ server แบบ requireTLS, (2) TLS พร้อม CA แต่ไม่มี credential, (3) TLS บวก credential, (4) ชื่อที่ certificate ไม่ครอบคลุม และ (5) สิ่งที่ไฟล์ config สร้างขึ้นจริง (output จริง รันกับ container ข้างบน):

scripts/sec-tls-checks.sh
M="docker exec -e ROOT_PW=<your-root-password> mongo-secure mongosh --quiet"
CA="tlsCAFile=/etc/mongo-lab/ca.pem"
echo "== 1) plain TCP to a requireTLS server"
$M "mongodb://localhost/?directConnection=true&serverSelectionTimeoutMS=3000" --eval 'db.hello().ok' 2>&1 | tail -2
echo "== 2) TLS, anonymous"
$M "mongodb://localhost/?tls=true&$CA&directConnection=true" --eval 'try { db.getSiblingDB("library").members.find().toArray() } catch (e) { console.log(e.codeName, e.code, "-", e.message) }'
echo "== 3) TLS + credentials"
$M "mongodb://root:<your-root-password>@localhost/?tls=true&$CA&authSource=admin&directConnection=true" --eval 'console.log("authenticated as", db.runCommand({ connectionStatus: 1 }).authInfo.authenticatedUsers[0].user)'
echo "== 4) a name that is not in the certificate"
$M "mongodb://sec-host/?tls=true&$CA&directConnection=true&serverSelectionTimeoutMS=3000" --eval 'db.hello().ok' 2>&1 | tail -2
echo "== 4b) same, verification switched off"
$M "mongodb://sec-host/?tls=true&$CA&directConnection=true&tlsAllowInvalidHostnames=true" --eval 'console.log("connected:", db.hello().ok === 1)' 2>&1 | tail -1
echo "== 5) what the config file produced"
$M "mongodb://root:<your-root-password>@localhost/?tls=true&$CA&authSource=admin&directConnection=true" --eval 'const p = db.serverCmdLineOpts().parsed; printjson({ security: p.security, net: p.net, replication: p.replication })'
== 1) plain TCP to a requireTLS server
MongoServerSelectionError: connection <monitor> to 127.0.0.1:27017 closed
== 2) TLS, anonymous
Unauthorized 13 - Command find requires authentication
== 3) TLS + credentials
authenticated as root
== 4) name not in the certificate
MongoServerSelectionError: Hostname/IP does not match certificate's altnames: Host: sec-host. is not in the cert's altnames: DNS:localhost, IP Address:127.0.0.1
== 4b) same, verification switched off
connected: true
== 5) what the config file produced
{
security: {
authorization: 'enabled',
keyFile: '/etc/mongo-lab/keyfile'
},
net: {
bindIp: '0.0.0.0',
port: 27017,
tls: {
CAFile: '/etc/mongo-lab/ca.pem',
allowConnectionsWithoutCertificates: true,
certificateKeyFile: '/etc/mongo-lab/server.pem',
mode: 'requireTLS'
}
},
replication: {
replSetName: 'rs0'
}
}

อ่านให้ละเอียด client ที่ไม่ใช้ TLS ถูกตัดก่อนที่จะพูดอะไรได้; client TLS ที่ไม่มี credential ได้ error requires authentication เดียวกับก่อนหน้า การตรวจ hostname ทำงาน — และ tlsAllowInvalidHostnames (หรือ tlsAllowInvalidCertificates) ทำให้ error หายไปด้วยการปิดการป้องกันนั่นเอง มันจึงเป็นเครื่องมือวินิจฉัยที่ต้องเอาออก ไม่ใช่วิธีแก้ที่เก็บไว้

TLS ปกป้องข้อมูล ระหว่างส่ง; encryption at rest ปกป้องไฟล์บนดิสก์ ใน MongoDB ที่ดูแลเอง encrypted storage engine “is available in MongoDB Enterprise only” (Encryption at rest, ตรวจเมื่อ 2026-09-30) ดังนั้นบน Community server ทางเลือกคือการเข้ารหัสระดับดิสก์หรือ filesystem ที่แพลตฟอร์มของคุณให้ บนคลาวด์ ให้ครอบ server ด้วย private network (VPC) และใช้ firewall หรือ security group ให้เฉพาะ application server เข้า port ได้

MongoDB Atlas คือบริการที่จัดการให้ และเอกสารของมันให้คำมั่นเหล่านี้ (ตรวจเมื่อ 2026-09-30): “Atlas requires TLS to encrypt the connections to your databases” (Set up cluster security); “Atlas only allows client connections to the cluster from entries in the project’s IP access list” (IP access list); ต้องสร้าง database user เพื่อเชื่อมต่อ; และ encryption at rest เปิดอยู่บน cluster ของมัน (Encryption at rest — ระบุไว้สำหรับ cluster แบบ Atlas Infinite) คอร์สนี้ไม่ได้รันอะไรบน Atlas (ไม่มีบัญชี); ทุกอย่างในย่อหน้านี้คือเอกสาร ไม่ใช่การวัด

ใน Compass / Atlas: คุณเชื่อม Compass ด้วย connection string ที่คัดลอกจาก UI ของ Atlas (ซึ่งมี TLS อยู่แล้ว); ใน Atlas, Database Access จัดการ user และ role และ Network Access จัดการ IP access list — ชั้นเดียวกันในรูปแบบฟอร์ม

  • อย่าเปิด port 27017 โดยไม่มี authentication container ที่เปิดโล่งข้างบนรับ insert จากทุกคนที่เข้าถึงได้
  • Least privilege user แบบอ่านอย่างเดียวอ่านได้และถูกปฏิเสธ insert; ให้แต่ละบริการมี user ของตัวเอง
  • ใส่ authSource=admin ใน URI เมื่อ user อยู่ใน admin
  • อย่าวาง tlsAllowInvalidCertificates=true เพื่อให้ error หายไป — มันปิดการตรวจที่ป้องกันผู้ปลอมตัว
  • TLS คือระหว่างส่ง, encryption at rest คือบนดิสก์; ต้องมีทั้งสอง keyfile ใช้ยืนยันตัวตนระหว่างสมาชิก

ผู้ช่วย AI ที่ถูกบอกว่า app user ถูกสร้าง “ใน admin database” เขียน connection string แบบตรงไปตรงมา:

🐛 หา bug ในโค้ดนี้

// AI comment: "credentials are in the URI, so this authenticates as library_app"
const client = new MongoClient("mongodb://library_app:" + pw + "@127.0.0.1:27304/library");
await client.connect();
await client.db("library").collection("members").findOne();
เฉลย

หากไม่มี authSource driver authenticate กับ database ใน URI path (library) ซึ่ง library_app ไม่มีอยู่ การรันจริงกับ container --auth:

// app/sec-authsource.mjs @skip-verify lab: needs the --auth container on 127.0.0.1:27304 (real output below)
import { MongoClient } from "mongodb";
const pw = process.env.APP_PW;
for (const [label, query] of [["bug (no authSource)", ""], ["fix (authSource=admin)", "?authSource=admin"]]) {
const client = new MongoClient(`mongodb://library_app:${pw}@127.0.0.1:27304/library${query}`);
try {
await client.connect();
console.log(label, "->", await client.db("library").collection("members").countDocuments());
} catch (e) { console.log(label, "->", e.name, e.codeName, e.code, "-", e.message); }
await client.close();
}
bug (no authSource) -> MongoServerError AuthenticationFailed 18 - Authentication failed.
fix (authSource=admin) -> 1

ความปลอดภัยคือ แถวของประตูอิสระ เครือข่ายตัดสินว่าใครเคาะประตูได้; TLS ตัดสินว่าเสียงเคาะถูกแอบฟังหรือปลอมได้ไหม; authentication ตัดสินว่าคุณคือใคร; role ตัดสินว่าตัวตนนั้นแตะอะไรได้; encryption at rest ตัดสินว่าดิสก์ที่ถูกขโมยมีค่าเท่าไร ทุกความล้มเหลวข้างบนคือประตูหนึ่งบานที่ปิดถูกต้อง — และทุก “วิธีแก้” ที่เปิดมันอีกครั้ง (--bind_ip_all บน port สาธารณะ, tlsAllowInvalid*, root ให้ทุกบริการ) คือการเอาประตูออก ไม่ใช่การแก้ปัญหา

ตัวเลือกประโยชน์ต้นทุน
MongoDB ที่ดูแลเองควบคุมโครงสร้างพื้นฐานและ config ได้เต็มที่คุณดูแล certificate และการหมุน, keyfile, user, patch, backup และ audit; encryption at rest ต้องใช้ Enterprise หรือแพลตฟอร์ม
MongoDB AtlasTLS, authentication และ IP access list เป็นค่าเริ่มต้นตามเอกสาร; งานดูแลน้อยลงควบคุมระดับล่างได้น้อยลง; ค่าใช้จ่ายตามการใช้งาน; ไม่ได้วัดที่นี่
หนึ่ง user ต่อบริการและ environmentรั่วหนึ่งบริการกระทบหนึ่งบริการuser ที่ต้องจัดการมากขึ้น
requireTLS กับ private CAclient ตรวจสอบ server ได้; ตัดการเชื่อมต่อธรรมดาต้องออกและหมุน certificate
  • “mongod เริ่มต้นปลอดภัยเพราะต้องใช้รหัสผ่าน” — container ที่เปิดโล่งแสดงรายการ database และรับ insert โดยไม่มี credential
  • “database ใน URI คือที่ที่ค้นหา user” — มันคือ authSource เริ่มต้น; user ที่สร้างใน admin ต้องใช้ ?authSource=admin (ไม่เช่นนั้น error code 18)
  • “passwordPrompt() ใส่ในสคริปต์ได้” — ใช้ได้เฉพาะใน shell แบบ interactive; สคริปต์อ่านความลับจาก environment หรือ secret manager
  • “TLS แปลว่าข้อมูลของฉันเข้ารหัสแล้ว” — ระหว่างส่งเท่านั้น; ดิสก์ต้องมี encryption at rest ซึ่งเป็นฟีเจอร์ Enterprise สำหรับ MongoDB ที่ดูแลเอง
  • “tlsAllowInvalidHostnames แก้ error ของ certificate” — มันปิดการตรวจที่ล้มเหลวนั่นเอง
  • “image ทางการ bind กับ localhost เหมือน binary” — มันสตาร์ทด้วย --bind_ip_all; publish port บน 127.0.0.1 หรือใช้ firewall

แท็บ Python/Go/Rust สะท้อนพฤติกรรมที่ตรวจแล้วใน shell/Node; ไม่ได้ถูกรันโดยตัวตรวจอัตโนมัติของคอร์ส

คุณสตาร์ท image mongo ทางการโดยไม่มี flag และ publish port client ที่ไม่ระบุตัวตนทำอะไรได้?
เมื่อเปิด --auth client ที่ไม่ระบุตัวตนรัน find error คืออะไร?
user ถูกสร้างใน database admin URI ของคุณคือ mongodb://user:pw@host/library โดยไม่มี authSource เกิดอะไรขึ้น?
user ที่มี role read บน library เรียก insertOne ผลคืออะไร?
client ได้ TLS error เรื่อง hostname ไม่ตรง การกระทำไหนแก้ที่ต้นเหตุ แทนที่จะซ่อนมัน?