SSD Nodes Learn Hosting plans →
คู่มือ Matt Connorโดย Matt Connor · อัปเดตเมื่อ 2026-08-28

วิธีตั้งค่าความปลอดภัย Ollama API ป้องกันพอร์ต 11434

Ollama API ไม่มีระบบตรวจสอบสิทธิ์โดยค่าเริ่มต้น ทำให้ผู้ที่เข้าถึงพอร์ต 11434 สั่งรันหรือลบโมเดลได้ทันที เรียนรู้วิธีป้องกันเซิร์ฟเวอร์ของคุณด้วย 3 ขั้นตอนที่ทำตามได้จริง

Ollama API ไม่มีรหัสผ่าน

Ollama API ไม่มีการตรวจสอบสิทธิ์ (authentication) ใดๆ ทั้งสิ้น ไม่มีการกำหนดผู้ใช้ ไม่มีรหัสผ่าน ไม่มีการตรวจสอบคีย์ และไม่มีรายการอนุญาต (allowlist) ในเซิร์ฟเวอร์ที่คุณใช้งาน ทุกสิ่งที่สามารถเปิดการเชื่อมต่อ TCP ไปยังพอร์ต 11434 สามารถแสดงรายการโมเดล สั่งรันโมเดล ดาวน์โหลดโมเดลใหม่ และลบโมเดลที่คุณมีอยู่ได้

เอกสารอย่างเป็นทางการระบุไว้อย่างชัดเจนว่า: "ไม่จำเป็นต้องมีการตรวจสอบสิทธิ์เมื่อเข้าถึง Ollama API ในเครื่องผ่าน http://localhost:11434" คำว่า ในเครื่อง (locally) คือรูปแบบความปลอดภัยทั้งหมดของระบบนี้ โดยค่าเริ่มต้น Ollama จะผูกการทำงานไว้กับ 127.0.0.1 ดังนั้นบนแล็ปท็อป อินเทอร์เฟซ loopback จึงทำหน้าที่เป็นตัวควบคุมการเข้าถึง หากคุณย้ายการรับฟัง (listener) ไปยังที่อยู่สาธารณะ การควบคุมการเข้าถึงจะหายไปทันที เพราะไม่มีกลไกอื่นมาทดแทน

นี่คือเหตุผลว่าทำไมเรื่องนี้จึงสำคัญบน VPS (virtual private server) ค่าเริ่มต้นนั้นมีความปลอดภัย แต่การเปลี่ยนแปลงแรกที่คนส่วนใหญ่มักทำ คือการเปิดการรับฟังเพื่อให้เครื่องที่สองสามารถใช้งานโมเดลได้ ซึ่งเป็นการเปลี่ยนแปลงที่ยกเลิกการป้องกันทั้งหมดในคราวเดียว

สิ่งที่พอร์ต 11434 ที่เปิดอยู่เปิดเผยออกมา

ทุก endpoint ไม่มีโหมดอ่านอย่างเดียวและไม่มีพอร์ตสำหรับผู้ดูแลระบบแยกต่างหาก นี่คือคำขอจริงที่มุ่งเป้าไปยังที่อยู่เซิร์ฟเวอร์แทนที่จะเป็น localhost:

# List every model on the box
curl http://SERVER_IP:11434/api/tags

# See what is loaded into memory right now
curl http://SERVER_IP:11434/api/ps

# Run a prompt on your hardware
curl http://SERVER_IP:11434/api/generate -d '{"model":"llama3.2","prompt":"Why is the sky blue?"}'

# Write several gigabytes to your disk
curl http://SERVER_IP:11434/api/pull -d '{"model":"llama3.2"}'

# Remove a model
curl -X DELETE http://SERVER_IP:11434/api/delete -d '{"model":"llama3.2"}'

ในมุมมองของผู้ดูแลระบบ มีสี่สิ่งที่ผิดพลาด:

  • CPU หรือ GPU ของคุณประมวลผล inference ให้ผู้อื่น ในแผนบริการที่มีการจำกัดการใช้งาน CPU หากมีการโหลดงานอย่างต่อเนื่อง หมายความว่าโควตาของคุณกำลังถูกใช้โดยคนแปลกหน้า และ การควบคุมค่าใช้จ่ายภาระงาน AI บน VPS จะทำได้ยากขึ้นมากเมื่อคุณไม่ใช่ผู้เรียกใช้งานเพียงคนเดียว
  • /api/pull เขียนข้อมูลลงในดิสก์ของคุณ โมเดลแต่ละตัวมีขนาดตั้งแต่ 2 ถึง 40 กิกะไบต์ การวนซ้ำคำสั่ง pull จะทำให้พื้นที่จัดเก็บข้อมูลเต็ม และดิสก์ที่เต็มจะทำให้บริการอื่นทุกตัวบนเครื่องหยุดทำงาน ไม่ใช่แค่ Ollama เท่านั้น
  • คำขอจะเข้ามาภายในกระบวนการของคุณและถูกบันทึกไว้ ในระดับ log เริ่มต้น Ollama จะบันทึกเฉพาะ metadata เท่านั้น ดังนั้นคุณจะได้ endpoint, สถานะ, latency และที่อยู่ของไคลเอนต์ ไม่ใช่ข้อความ prompt แต่นั่นก็ยังคงเป็นบันทึกว่าใครใช้งานเครื่องของคุณและใช้งานเพื่ออะไร ซึ่งค้างอยู่ใน journal ของคุณโดยที่คุณไม่ได้เลือกที่จะจัดเก็บข้อมูลนั้น
  • /api/delete ลบโมเดล การจะนำโมเดลเหล่านั้นกลับมาหมายถึงการต้องดาวน์โหลดใหม่ผ่านแบนด์วิดท์ของคุณเอง

ทั้งหมดนี้ไม่จำเป็นต้องอาศัยช่องโหว่ใดๆ นี่คือ API ตามเอกสารที่ทำงานตามที่ออกแบบไว้ทุกประการ

กุญแจ Ed25519 ไม่ใช่การควบคุมการเข้าถึง

เมื่อค้นหาคำว่า "Ollama API key" คุณจะพบกับสองสิ่งที่แตกต่างกัน ทั้งสองอย่างไม่ใช่รหัสผ่านสำหรับเซิร์ฟเวอร์ของคุณ การแยกแยะความแตกต่างเหล่านี้จะช่วยลดความสับสนได้เกือบทั้งหมด

อย่างแรกคือคู่กุญแจระบุตัวตน (identity key pair) Ollama จะสร้างคู่กุญแจ Ed25519 ขึ้นมาในการรันครั้งแรก บน Linux สคริปต์ติดตั้งจะสร้างผู้ใช้ระบบชื่อ ollama โดยมีโฮมไดเรกทอรีอยู่ที่ /usr/share/ollama ดังนั้นคู่กุญแจจึงอยู่ที่นี่:

/usr/share/ollama/.ollama/id_ed25519
/usr/share/ollama/.ollama/id_ed25519.pub

กุญแจนี้ชี้ออกไปยังภายนอก ollama signin จะลงทะเบียนกุญแจส่วนสาธารณะ (public half) ไว้กับบัญชี ollama.com ของคุณ และเป็นสิ่งที่อนุญาตให้คุณ push โมเดลไปยัง registry หรือ pull โมเดลส่วนตัวออกมาได้ มันทำหน้าที่พิสูจน์ตัวตนเครื่องของคุณต่อ ollama.com และไม่ได้ร้องขอสิ่งใดจากไคลเอนต์ที่เชื่อมต่อเข้ามายังเครื่องของคุณ การลบกุญแจ การเปลี่ยนกุญแจ หรือการไม่สร้างกุญแจนี้เลย ไม่ส่งผลใดๆ ต่อผู้ที่จะเรียกใช้ API ของคุณ

อย่างที่สองคือ OLLAMA_API_KEY ตัวแปรนี้เก็บกุญแจที่คุณสร้างขึ้นที่ https://ollama.com/settings/keys และไคลเอนต์ของคุณจะส่งกุญแจนี้ในรูปแบบ Authorization: Bearer $OLLAMA_API_KEY เมื่อเรียกใช้ API ที่โฮสต์ไว้ที่ https://ollama.com/api มันเป็นข้อมูลรับรอง (credential) สำหรับบริการของพวกเขา ซึ่งคุณใช้ในฐานะไคลเอนต์ ollama serve ของคุณเองไม่เคยอ่านค่านี้ การตั้งค่า OLLAMA_API_KEY บน VPS ของคุณไม่ได้เป็นการใส่รหัสผ่านให้กับ VPS ของคุณ

ดังนั้นจึงไม่มีการตั้งค่าใดให้เปิดใช้งาน วิธีป้องกันทั้งสามแบบด้านล่างนี้ทำงานในลักษณะเดียวกัน คือการทำให้พอร์ตไม่สามารถเข้าถึงได้จากภายนอก และวางระบบอื่นไว้ด้านหน้าเพื่อทำหน้าที่ตรวจสอบสิทธิ์แทน

ตรวจสอบว่าเซิร์ฟเวอร์ของคุณกำลังฟังพอร์ตใดอยู่บ้างในขณะนี้

sudo ss -tlnp | grep 11434

ผลลัพธ์ที่ปลอดภัยจะระบุว่าเป็น loopback address:

LISTEN 0  4096  127.0.0.1:11434  0.0.0.0:*  users:(("ollama",pid=812,fd=3))

ผลลัพธ์ที่เปิดเผยต่อสาธารณะจะระบุทุกอินเทอร์เฟซ:

LISTEN 0  4096  0.0.0.0:11434  0.0.0.0:*  users:(("ollama",pid=812,fd=3))

0.0.0.0 หมายถึงที่อยู่ IPv4 ทั้งหมดบนเครื่อง รวมถึงที่อยู่สาธารณะด้วย ส่วน *:11434 และ [::]:11434 หมายถึงสิ่งเดียวกันโดยรวม IPv6 เข้าไปด้วย

ตอนนี้ให้ยืนยันจากภายนอก โดยรันคำสั่งนี้บนแล็ปท็อปของคุณ ไม่ใช่บนเซิร์ฟเวอร์:

curl -m 5 http://YOUR_SERVER_IP:11434/api/version

curl: (28) Connection timed out after 5001 milliseconds คือคำตอบที่คุณต้องการ เช่นเดียวกับ curl: (7) Failed to connect ... Connection refused หากออบเจกต์ JSON มีฟิลด์ version แสดงว่า API ทั้งหมดสามารถเข้าถึงได้โดยใครก็ตามที่ร้องขอ การทดสอบด้วย curl บนตัวเซิร์ฟเวอร์เองนั้นไม่สามารถพิสูจน์อะไรได้ เพราะ loopback จะตอบกลับเสมอ

การเปิดเผยข้อมูลมักเกิดขึ้นจาก 2 สาเหตุหลัก สาเหตุแรกคือการแก้ไขไฟล์โดยเจตนา เนื่องจากมีความจำเป็นต้องให้เครื่องอื่นเข้าถึงโมเดลได้:

sudo systemctl edit ollama.service
[Service]
Environment="OLLAMA_HOST=0.0.0.0:11434"

บรรทัดเดียวนั้นคือสาเหตุของการเปิดเผยทั้งหมด สาเหตุที่สองคือ Docker ซึ่งไม่ได้ร้องขอให้คุณแก้ไขไฟล์ใดๆ เลย และเรื่องนั้นมีส่วนอธิบายแยกต่างหากด้านล่างนี้

การป้องกันที่ 1: จำกัดไว้ที่ localhost และใช้ tunnel เข้าไป

ให้ใช้วิธีนี้เป็นอันดับแรก เนื่องจากไม่ต้องติดตั้งซอฟต์แวร์ใหม่และไม่สร้างข้อมูลรับรองที่อาจรั่วไหลได้ พอร์ตดังกล่าวจะไม่ปรากฏบนอินเทอร์เฟซสาธารณะ ทำให้การสแกนหาพอร์ตไม่พบ

ให้กำหนด bind address อย่างชัดเจนแทนการพึ่งพาค่าเริ่มต้น:

sudo systemctl edit ollama.service
[Service]
Environment="OLLAMA_HOST=127.0.0.1:11434"

คำสั่งดังกล่าวจะเขียนไฟล์ /etc/systemd/system/ollama.service.d/override.conf ให้ใช้การตั้งค่าและตรวจสอบ:

sudo systemctl daemon-reload
sudo systemctl restart ollama
sudo ss -tlnp | grep 11434

ss ควรแสดงผลเป็น 127.0.0.1:11434 หากยังคงแสดงเป็น 0.0.0.0 แสดงว่ามีไฟล์ drop-in อื่นที่ทับซ้อนอยู่ ให้รัน systemctl cat ollama.service เพื่อแสดงรายการ unit และไฟล์ drop-in ทั้งหมดพร้อมพาธ จากนั้นให้ลบไฟล์ที่ไม่ได้ใช้งานออก

หากต้องการใช้งานโมเดลจากแล็ปท็อปของคุณ ให้ส่งต่อพอร์ตผ่าน SSH:

ssh -N -L 11434:127.0.0.1:11434 you@your-server

-L 11434:127.0.0.1:11434 จะเปิดพอร์ต 11434 บนแล็ปท็อปของคุณและส่งข้อมูลทั้งหมดที่เข้ามาไปยัง 127.0.0.1:11434 ตามที่มองเห็นจากฝั่งเซิร์ฟเวอร์ -N เป็นการบอกให้ SSH ไม่ต้องรันคำสั่งระยะไกล เพื่อให้กระบวนการคงสถานะ tunnel ไว้ ในขณะที่รันคำสั่งนี้ คุณจะสามารถใช้งานบนแล็ปท็อปได้ดังนี้:

curl -s http://localhost:11434/api/tags

คุณอาจพบความล้มเหลว 2 ประการ: bind [127.0.0.1]:11434: Address already in use หมายความว่าแล็ปท็อปของคุณกำลังรัน Ollama ของตัวเองบนพอร์ตนั้นอยู่ ให้เลือกพอร์ตในเครื่องอื่นด้วย -L 11500:127.0.0.1:11434 แล้วชี้ไคลเอนต์ของคุณไปที่ 11500 หากได้รับข้อความตอบกลับว่างเปล่าผ่าน tunnel ที่เชื่อมต่อได้ปกติ หมายความว่า SSH ทำงานได้ถูกต้องแต่ Ollama ไม่ได้ฟังพอร์ตที่ฝั่งเซิร์ฟเวอร์ ให้ตรวจสอบ ss ที่นั่นก่อนที่จะแก้ไขคำสั่ง SSH

สำหรับเครื่องไคลเอนต์หลายเครื่อง การใช้เครือข่ายส่วนตัวจะดีกว่าการสร้าง tunnel แยกรายบุคคล ให้เชื่อมต่อเครื่องเหล่านั้นเข้ากับ WireGuard หรือ Tailscale จากนั้นจึง bind Ollama เข้ากับที่อยู่บนเครือข่ายนั้นแทนที่จะเป็น 0.0.0.0:

[Service]
Environment="OLLAMA_HOST=10.8.0.1:11434"

พอร์ตจะปรากฏเฉพาะบนอินเทอร์เฟซที่คุณต้องมีกุญแจในการเข้าถึงเท่านั้น วิธีนี้ยังช่วยป้องกันความผิดพลาดจาก firewall ได้ด้วย เนื่องจากกฎที่อนุญาตให้เข้าถึงจากภายนอกโดยไม่ตั้งใจ ก็ยังไม่สามารถเปิดเผย listener ที่ไม่ได้อยู่บนอินเทอร์เฟซสาธารณะได้

การป้องกันที่ 2: การใช้ reverse proxy ตรวจสอบ bearer token

เมื่อจำเป็นต้องให้บริการจากอินเทอร์เน็ตสาธารณะเรียกใช้งานโมเดล ให้คงการตั้งค่า Ollama ไว้ที่ loopback และวาง proxy ไว้ด้านหน้าเพื่อทำหน้าที่แทน โดย proxy จะทำหน้าที่ทำ TLS (transport layer security) termination และปฏิเสธคำขอที่ไม่มี header ที่ถูกต้อง ในขณะที่ Ollama จะยังคงยอมรับการเชื่อมต่อจาก 127.0.0.1 เท่านั้น ดังนั้น proxy จึงเป็นช่องทางเดียวที่เข้าถึงได้

ขั้นแรกให้สร้าง token จริงขึ้นมา ห้ามสร้างขึ้นเองด้วยมือ:

openssl rand -base64 36

ตัวอย่างการตั้งค่า nginx เพื่อตรวจสอบ token:

map $http_authorization $ollama_ok {
    default                                   0;
    "Bearer PASTE_YOUR_GENERATED_TOKEN_HERE"  1;
}

server {
    listen 443 ssl;
    server_name llm.example.com;

    ssl_certificate     /etc/letsencrypt/live/llm.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/llm.example.com/privkey.pem;

    location = /api/pull   { return 403; }
    location = /api/delete { return 403; }
    location = /api/push   { return 403; }

    location / {
        if ($ollama_ok = 0) { return 401; }

        proxy_pass http://127.0.0.1:11434;
        proxy_set_header Host 127.0.0.1:11434;
        proxy_buffering off;
        proxy_read_timeout 600s;
    }
}

มี 5 บรรทัดที่ทำหน้าที่สำคัญ และแต่ละบรรทัดช่วยป้องกันความล้มเหลวที่คุณอาจพบได้หากไม่ใช้งาน

การใช้ if ภายในบล็อก location โดยปกติแล้วไม่ใช่แนวทางที่ดีใน nginx แต่การระบุ body เป็น return โดยตรงถือเป็นหนึ่งในสองรูปแบบที่ทำงานได้อย่างคาดการณ์ได้ ดังนั้นการใช้งานในลักษณะนี้จึงปลอดภัย

location = /api/pull เป็นการจับคู่แบบตรงตัว (exact match) และ nginx จะให้ความสำคัญกับการจับคู่แบบตรงตัวสูงกว่า prefix location / ดังนั้น endpoint ทั้งสามรายการนั้นจะถูกปฏิเสธก่อนที่จะมีการตรวจสอบ token เสียอีก การมี token ที่ถูกต้องจึงหมายถึงการได้รับสิทธิ์ในการประมวลผล (inference) ไม่ใช่สิทธิ์ในการเขียนข้อมูลจนเต็มดิสก์ของคุณ

proxy_set_header Host 127.0.0.1:11434; มีความสำคัญเนื่องจาก Ollama จะตรวจสอบ header Host และ Origin ที่ส่งเข้ามา การส่งผ่าน hostname สาธารณะของ proxy โดยตรงอาจทำให้เกิด 403 Forbidden ที่มาจาก Ollama แทนที่จะมาจาก nginx ซึ่งจะทำให้การตรวจสอบปัญหาทำได้ยาก ส่วน OLLAMA_ORIGINS เป็นอีกหนึ่งตัวแปรที่สำคัญสำหรับไคลเอนต์เบราว์เซอร์ที่ต้องการอนุญาต origin เฉพาะเจาะจง

proxy_buffering off; มีความสำคัญเนื่องจาก Ollama จะส่งการตอบกลับแบบสตรีมทีละ token หากเปิดใช้งานการบัฟเฟอร์ nginx จะกักเก็บสตรีมไว้และส่งมอบข้อมูลทั้งหมดในคราวเดียวเมื่อเสร็จสิ้น ทำให้ไคลเอนต์ของคุณดูเหมือนค้างไปตลอดระยะเวลาการสร้างข้อมูล

proxy_read_timeout 600s; มีความสำคัญเนื่องจาก nginx มีค่าเริ่มต้นที่ 60 วินาที การสร้างข้อมูลที่ใช้เวลานานบน CPU มักจะเกินเวลานี้ได้ง่าย ทำให้ไคลเอนต์ได้รับ 504 Gateway Time-out และ /var/log/nginx/error.log จะบันทึก upstream timed out (110: Connection timed out) while reading response header from upstream ทั้งที่คำขอดังกล่าวยังคงทำงานอยู่ แต่ nginx ได้ยกเลิกการรอไปแล้ว

โหลดการตั้งค่าใหม่และทดสอบทั้งสองเส้นทาง:

sudo nginx -t && sudo systemctl reload nginx
curl -s -o /dev/null -w '%{http_code}\n' https://llm.example.com/api/tags
curl -s -H "Authorization: Bearer YOUR_TOKEN" https://llm.example.com/api/tags

คำสั่งแรกควรแสดงผล 401 ส่วนคำสั่งที่สองควรแสดงรายการโมเดลของคุณ หากคำสั่งแรกส่งคืนรายการโมเดลด้วย แสดงว่าบล็อก map อยู่ในขอบเขต (scope) ที่ผิด ซึ่งควรจะอยู่ที่ระดับ http ดังนั้นให้วางไว้ในไฟล์ภายใต้ /etc/nginx/conf.d/ หรือเหนือบล็อก server ห้ามวางไว้ภายใน server โดยเด็ดขาด

Caddy สามารถทำงานเดียวกันนี้ได้ด้วยการทำ basic authentication โดยใช้เพียง 4 บรรทัด ซึ่งเหมาะสมกับไคลเอนต์เบราว์เซอร์มากกว่าการใช้ bearer token:

llm.example.com {
	basic_auth {
		apiuser PASTE_BCRYPT_HASH_HERE
	}
	reverse_proxy 127.0.0.1:11434
}

ให้รันคำสั่ง caddy hash-password เพื่อสร้าง bcrypt hash ตามที่ Caddy ต้องการ ข้อควรระวังเรื่องชื่อคำสั่ง: directive นี้เคยใช้ชื่อ basicauth ก่อน Caddy v2.8 และปัจจุบันเปลี่ยนเป็น basic_auth ดังนั้นหากคัดลอกการตั้งค่าจากคู่มือเก่ามาใช้ โปรแกรมจะไม่สามารถโหลดได้และ Caddy จะแจ้งชื่อ directive ที่ไม่รู้จักออกมา

ไม่ว่าคุณจะเลือกใช้ proxy แบบใด นี่คือความลับร่วม (shared secret) สำหรับทุกคน ไคลเอนต์ทุกตัวที่ถือ token นี้จะมีสิทธิ์เข้าถึงเท่ากันทั้งหมด และการเพิกถอนสิทธิ์หมายถึงการต้องแก้ไขไฟล์ config และอัปเดตไคลเอนต์ทุกตัวพร้อมกันในคราวเดียว

การป้องกันที่ 3: เกตเวย์ที่ออกคีย์แยกตามไคลเอนต์

เมื่อมีบุคคลหรือแอปพลิเคชันมากกว่าหนึ่งรายเรียกใช้งานโมเดล โทเค็นที่ใช้ร่วมกันจะเริ่มไม่เพียงพอ คุณจะไม่สามารถระบุได้ว่าไคลเอนต์รายใดเป็นผู้สร้างภาระงาน และไม่สามารถตัดการเชื่อมต่อรายใดรายหนึ่งได้โดยไม่ส่งผลกระทบต่อทั้งหมด เกตเวย์จะทำหน้าที่แทนที่พร็อกซี โดยสื่อสารผ่าน API ที่รองรับ OpenAI เหมือนกัน ออกคีย์แยกต่างหากสำหรับแต่ละไคลเอนต์ และบันทึกปริมาณการใช้งานของแต่ละคีย์ เกตเวย์ LiteLLM แบบ self-hosted เป็นคำตอบที่นิยมใช้ ซึ่งจะเพิ่มฟีเจอร์การกำหนดงบประมาณต่อคีย์และบันทึกคำขอเพิ่มเติมจากการควบคุมการเข้าถึง

กฎจากการป้องกันที่ 1 ยังคงเดิม Ollama ต้องผูกกับ 127.0.0.1 โดยให้เกตเวย์เป็นกระบวนการเดียวที่สื่อสารกับ Ollama และเกตเวย์ต้องเป็นบริการเดียวที่เปิดรับการเชื่อมต่อจากสาธารณะ การติดตั้งเกตเวย์บนเครื่องที่ยังเปิดพอร์ต 11434 สู่สาธารณะถือเป็นการกระทำที่ไร้ผล เพราะผู้เรียกใช้งานสามารถข้ามผ่านเกตเวย์ไปได้โดยตรง

กับดักของไฟร์วอลล์: พอร์ตของคอนเทนเนอร์ที่ถูก publish จะข้ามการทำงานของ UFW

นี่คือเหตุผลว่าทำไมอินสแตนซ์ที่เปิดเผยต่อสาธารณะจึงปรากฏบนเซิร์ฟเวอร์ที่เจ้าของตั้งค่าไฟร์วอลล์ไว้อย่างถูกต้องแล้ว

UFW (uncomplicated firewall) จะเขียนกฎลงใน chain INPUT ของตาราง filter ในเคอร์เนล และ INPUT จะจัดการแพ็กเก็ตที่ส่งถึงตัวโฮสต์โดยตรง แต่แฟล็ก -p ของ Docker จะเขียนกฎ destination NAT (network address translation) ลงใน chain PREROUTING ของตาราง nat ซึ่งเคอร์เนลจะประเมินกฎนี้ก่อนที่จะตัดสินใจว่าแพ็กเก็ตจะไปที่ใด เมื่อถึงขั้นตอนการตัดสินใจเรื่องเส้นทาง (routing decision) ปลายทางจะถูกเขียนทับเป็นที่อยู่ของคอนเทนเนอร์ไปแล้ว แพ็กเก็ตจึงถูกส่งต่อ (forward) แทนที่จะถูกส่งไปยังเครื่องโฮสต์โดยตรง และมันจะผ่าน FORWARD แทนที่จะเป็น INPUT กฎ INPUT ของ UFW จึงไม่ถูกนำมาพิจารณา ทำให้แพ็กเก็ตอ้อมผ่านไฟร์วอลล์ไปแทนที่จะผ่านการตรวจสอบ

นี่คือเหตุผลที่ลำดับคำสั่งนี้ทำให้พอร์ต 11434 เปิดรับการเชื่อมต่อจากอินเทอร์เน็ต:

sudo ufw default deny incoming
sudo ufw enable
docker run -d -v ollama:/root/.ollama -p 11434:11434 --name ollama ollama/ollama

และ sudo ufw status ยังคงรายงานว่าไฟร์วอลล์ทำงานอยู่และตั้งค่าเป็น default deny ข้อมูลทั้งสองชุดถูกต้องพร้อมกัน ซึ่งเป็นเหตุผลว่าทำไมผู้ใช้จึงเชื่อถือข้อมูลที่ผิด คุณสามารถตรวจสอบกฎที่เป็นต้นเหตุได้ด้วยคำสั่งนี้:

sudo iptables -t nat -L DOCKER -n

วิธีแก้ไขคือการระบุที่อยู่ (address) ในแฟล็ก publish:

docker rm -f ollama
docker run -d -v ollama:/root/.ollama -p 127.0.0.1:11434:11434 --name ollama ollama/ollama

-p 11434:11434 เป็นตัวย่อของ -p 0.0.0.0:11434:11434 การระบุ 127.0.0.1 จะเป็นการผูกฝั่งโฮสต์ของการแมปพอร์ตไว้กับ loopback ทำให้ SSH tunnel และ reverse proxy ของคุณยังคงเข้าถึงบริการได้ แต่อินเทอร์เน็ตภายนอกไม่สามารถเข้าถึงได้ การสร้างคอนเทนเนอร์ใหม่ในกรณีนี้มีความปลอดภัยเนื่องจากโมเดลถูกเก็บไว้ใน volume ที่ชื่อ ollama ไม่ได้อยู่ภายในคอนเทนเนอร์

ตรวจสอบให้แน่ใจว่าทั้งสองมุมมองแสดงผลตรงกัน:

docker port ollama
sudo ss -tlnp | grep 11434

docker port ollama ควรแสดงผลเป็น 11434/tcp -> 127.0.0.1:11434 หากแสดงผลเป็น 0.0.0.0:11434 แสดงว่าคุณยังคงเปิดเผยบริการต่อสาธารณะอยู่ ทำความเข้าใจกลไกนี้เพียงครั้งเดียวแล้วคุณจะสามารถนำไปใช้กับทุกคอนเทนเนอร์ที่คุณ publish: เหตุใดพอร์ตที่ Docker publish จึงข้าม UFW จะอธิบายถึง chain DOCKER-USER และกฎที่ยังคงอยู่แม้จะรีสตาร์ท Docker หากคุณกำลังสร้างนโยบายความปลอดภัยสำหรับโฮสต์ กฎ UFW ที่ VPS ใหม่จำเป็นต้องมี จะครอบคลุมถึงพื้นฐานที่จำเป็น สำหรับ Rocky หรือ AlmaLinux จะไม่มี UFW ให้ตั้งค่า ดังนั้น นโยบายพื้นฐานเดียวกันที่เขียนด้วย firewalld จึงเป็นจุดเริ่มต้นที่เหมาะสมกว่า

กระบวนการทำงานภายใต้สิทธิ์ของผู้ใช้ใด

สคริปต์ติดตั้งบน Linux จะสร้างบัญชีผู้ใช้เฉพาะขึ้นมาและรันเซอร์วิสภายใต้บัญชีนั้น:

useradd -r -s /bin/false -U -m -d /usr/share/ollama ollama

ไฟล์ unit ที่ /etc/systemd/system/ollama.service จะกำหนดค่า User=ollama และ Group=ollama ไว้แล้ว ไม่ควรแก้ไขส่วนนี้ การรัน ollama serve ด้วยตนเองในเทอร์มินัลจะรันภายใต้สิทธิ์ของผู้ใช้ที่คุณล็อกอินอยู่ หากคุณล็อกอินเป็น root จะส่งผลให้ API ที่ไม่มีการตรวจสอบสิทธิ์เขียนไฟล์ด้วยสิทธิ์ของ root ให้ตรวจสอบว่าสถานะเป็นอย่างไร:

ps -o user= -C ollama

ผลลัพธ์ควรแสดงเป็น ollama หากเป็นค่าอื่น แสดงว่ามีกระบวนการที่รันด้วยตนเองทำงานซ้อนอยู่หรือทำงานแทนที่ unit หลัก แนวคิดเดียวกันนี้ใช้กับ daemon ทุกตัวที่คุณจะเพิ่มในภายหลัง และ การรันเซอร์วิสด้วยสิทธิ์ผู้ใช้ที่จำกัด (least-privilege) จะอธิบายรายละเอียดเรื่องนี้ไว้อย่างถูกต้อง

วิธีตรวจสอบความปลอดภัยของ Ollama API endpoint

ไม่ว่าคุณจะเลือกวิธีใด การทดสอบหนึ่งอย่างจะช่วยยืนยันผลได้ โดยต้องรันคำสั่งจากเครื่องอื่น:

curl -m 5 http://YOUR_SERVER_IP:11434/api/version
curl -m 5 http://YOUR_SERVER_IP:11434/api/tags

ทั้งสองคำสั่งควรจะ time out หรือถูกปฏิเสธการเชื่อมต่อ หากคุณสร้าง proxy ไว้ ทั้งสอง path เดียวกันบน hostname ของ proxy ควรส่งค่า 401 กลับมาหากไม่มีการระบุข้อมูลยืนยันตัวตน และส่ง JSON จริงกลับมาหากมีการระบุข้อมูลที่ถูกต้อง

จากนั้นให้อ่าน access log หนึ่งครั้ง เพราะ log จะบอกคุณว่ามีใครพบพอร์ตนี้ในขณะที่มันเปิดอยู่หรือไม่:

journalctl -u ollama --since "-30 days" | grep GIN | grep -v 127.0.0.1

Ollama จะเขียน log หนึ่งบรรทัดต่อหนึ่งคำขอและระบุที่อยู่ของ client ไว้ด้วย:

[GIN] 2026/08/12 - 14:01:10 | 200 | 103.965898ms | 127.0.0.1 | POST "/api/generate"

ทุกบรรทัดควรแสดงค่า 127.0.0.1 เมื่อ Ollama ถูกผูกไว้กับ loopback แล้ว เพราะนั่นเป็นที่อยู่เดียวที่การเชื่อมต่อสามารถส่งเข้ามาได้ หากพบที่อยู่สาธารณะในคอลัมน์นั้น แสดงว่าเป็นคำขอที่มาจากภายนอก และ timestamp จะบอกคุณว่าเกิดขึ้นเมื่อใด ผลลัพธ์ที่คุณต้องการคือไม่ควรมี output ใดๆ ปรากฏขึ้นจากคำสั่งดังกล่าว หากคุณยังใหม่กับส่วนของโมเดล การรัน Ollama บน VPS จะครอบคลุมเนื้อหาเรื่องการติดตั้ง การเลือกขนาดโมเดล และขีดจำกัดของหน่วยความจำที่จะเป็นตัวตัดสินว่าโมเดลใดสามารถโหลดขึ้นมาใช้งานได้จริง

FAQ

Does Ollama have an API key or a password?

No. The server you run has no authentication of any kind, and the official documentation states that no authentication is required to reach the API. Both things called an "Ollama API key" point the other way. The Ed25519 pair in /usr/share/ollama/.ollama/ proves your machine to ollama.com so you can push models and pull private ones. OLLAMA_API_KEY is a credential your client sends to the hosted API at https://ollama.com/api. Your own ollama serve reads neither one, so access control has to come from the network or from a proxy in front.

Is OLLAMA_HOST=0.0.0.0 safe if I have a firewall?

Only while nothing else writes firewall rules on that box. 0.0.0.0 means the listener really exists on the public interface, and you are trusting the firewall alone to keep it unreachable. That trust breaks the moment Docker publishes a port, because the DNAT rule Docker adds to the nat table is evaluated before the packet would reach the INPUT chain where UFW lives, so the packet is forwarded and UFW never sees it. Binding to 127.0.0.1 or to a private tunnel address removes the listener from the public interface, so a firewall mistake has nothing left to expose.

How do I check whether my Ollama port is open to the internet?

Run sudo ss -tlnp | grep 11434 on the server, and curl -m 5 http://YOUR_SERVER_IP:11434/api/version from a different machine. ss showing 127.0.0.1:11434 and the remote curl timing out is the pair of answers you want. ss showing 0.0.0.0:11434 or *:11434 while the remote curl returns JSON means the full API is reachable. Never test with curl on the server itself, because loopback answers whatever the bind address happens to be.

Can I just move the port from 11434 to something random?

No, and the reason is worth stating. A different port slows down nothing except a scan of one single port. Scanners walk the whole range, and one request to /api/tags identifies the service whatever port it arrived on. Moving the port also breaks every client default and makes your own setup harder to reason about later. Bind to loopback instead, which removes the listener rather than relocating it.

Someone reached my open Ollama. What should I check?

Bind it to 127.0.0.1 and restart the service first, so the exposure stops before you start investigating. Then run journalctl -u ollama --since "-30 days" | grep GIN | grep -v 127.0.0.1 to see which outside addresses called which endpoints and when. Compare ollama list against the models you meant to have, since /api/pull is unauthenticated and a model you did not pull is both disk usage and evidence. Check free space with df -h. Ollama does not record prompt text at the default log level, so you have a record of who asked and for which model, not of what was generated.