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 เชื่อใจทุกคนภายใน
ขั้น 0 — ค่าเริ่มต้นคือเปิดโล่ง
หัวข้อที่มีชื่อว่า “ขั้น 0 — ค่าเริ่มต้นคือเปิดโล่ง”container mongo:8.3.11 ธรรมดา ไม่มี flag (publish บน loopback ของ host เท่านั้น):
docker run -d --name mongo-open --memory 512m -p 127.0.0.1:27304:27017 mongo:8 mongod --wiredTigerCacheSizeGB 0.25docker 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 (RBAC)
หัวข้อที่มีชื่อว่า “Authentication และ role (RBAC)”Authentication พิสูจน์ว่าคุณเป็นใคร; role-based access control จำกัดสิ่งที่คุณทำได้ หลักการคือ least privilege image สร้าง user แรก (root) จาก environment variable ให้ได้ บน data directory ที่ว่างเท่านั้น และสตาร์ทด้วย --auth:
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 authenticationMongoServerError 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: 1Unauthorized 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
mongosh "mongodb://library_app@localhost:27017/library?authSource=admin" --password "$APP_PW"// app/sec-connect.mjs @skip-verify lab: needs the --auth container on 127.0.0.1:27304import { MongoClient } from "mongodb";
const uri = `mongodb://library_app:${process.env.APP_PW}@127.0.0.1:27304/library?authSource=admin`;const client = new MongoClient(uri);await client.connect();console.log(await client.db("library").collection("members").countDocuments());await client.close();uri = "mongodb://library_app:<your-password>@host:27017/library?authSource=admin&tls=true"client = MongoClient(uri)uri := "mongodb://library_app:<your-password>@host:27017/library?authSource=admin&tls=true"client, err := mongo.Connect(options.Client().ApplyURI(uri))if err != nil { return err}let uri = "mongodb://library_app:<your-password>@host:27017/library?authSource=admin&tls=true";let client = Client::with_uri_str(uri).await?;Keyfile, TLS และ mongod.conf
หัวข้อที่มีชื่อว่า “Keyfile, TLS และ mongod.conf”อีกสามชิ้น รวมกันในไฟล์ 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:
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.extopenssl x509 -req -in server.csr -CA ca.pem -CAkey ca.key -CAcreateserial -out server.crt -days 2 -extfile san.extcat server.key server.crt > server.pemopenssl rand -base64 756 > keyfile# mongod.confsecurity: authorization: enabled keyFile: /etc/mongo-lab/keyfilenet: port: 27017 bindIp: 0.0.0.0 tls: mode: requireTLS certificateKeyFile: /etc/mongo-lab/server.pem CAFile: /etc/mongo-lab/ca.pem allowConnectionsWithoutCertificates: truereplication: replSetName: rs0storage: wiredTiger: engineConfig: cacheSizeGB: 0.25allowConnectionsWithoutCertificates: 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 จริง):
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():
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 ข้างบน):
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 -2echo "== 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 -2echo "== 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 -1echo "== 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 serverMongoServerSelectionError: connection <monitor> to 127.0.0.1:27017 closed== 2) TLS, anonymousUnauthorized 13 - Command find requires authentication== 3) TLS + credentialsauthenticated as root== 4) name not in the certificateMongoServerSelectionError: 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 offconnected: 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 หายไปด้วยการปิดการป้องกันนั่นเอง มันจึงเป็นเครื่องมือวินิจฉัยที่ต้องเอาออก ไม่ใช่วิธีแก้ที่เก็บไว้
Encryption at rest, เครือข่าย และ Atlas
หัวข้อที่มีชื่อว่า “Encryption at rest, เครือข่าย และ Atlas”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 สร้าง
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI สร้าง”ผู้ช่วย 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) -> 1Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”ความปลอดภัยคือ แถวของประตูอิสระ เครือข่ายตัดสินว่าใครเคาะประตูได้; 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 Atlas | TLS, authentication และ IP access list เป็นค่าเริ่มต้นตามเอกสาร; งานดูแลน้อยลง | ควบคุมระดับล่างได้น้อยลง; ค่าใช้จ่ายตามการใช้งาน; ไม่ได้วัดที่นี่ |
| หนึ่ง user ต่อบริการและ environment | รั่วหนึ่งบริการกระทบหนึ่งบริการ | user ที่ต้องจัดการมากขึ้น |
requireTLS กับ private CA | client ตรวจสอบ server ได้; ตัดการเชื่อมต่อธรรมดา | ต้องออกและหมุน certificate |
Common Misconceptions
หัวข้อที่มีชื่อว่า “Common Misconceptions”- “
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; ไม่ได้ถูกรันโดยตัวตรวจอัตโนมัติของคอร์ส