EST. 2026 — บันทึกงานเซิร์ฟเวอร์จริง SYSADMIN · SECURITY · AI · DIRECTADMIN เล่มที่ 01
THE SYSADMIN ALMANAC
หน้าหลัก / Security / ตัดความยาวก่อนปิดบัง token เลยหลุดลง log ไป 31 ตัวอักษร
Security

ตัดความยาวก่อนปิดบัง token เลยหลุดลง log ไป 31 ตัวอักษร

โดย Ton-Or ·12 พ.ย. 2026·อ่าน 11 นาที
ตัดความยาวก่อนปิดบัง token เลยหลุดลง log ไป 31 ตัวอักษร

ตัวปิดบังความลับที่ค้นหา token ทั้งก้อน จะมองไม่เห็น token ที่ถูกตัดเหลือครึ่งก้อน และเทสต์ที่ถามว่า log มี token ทั้งก้อนไหม ก็จะตอบว่าไม่มีอย่างซื่อสัตย์ ทั้งที่ครึ่งหนึ่งของมันนอนอยู่ในไฟล์

สรุปใน 1 นาที

อาการ/ปัญหา
token ที่ใส่ผิดช่องหลุดลง log เป็น hex 31 ตัวอักษร ทั้งที่มีตัว redact และเทสต์เขียว
ใครควรอ่าน
คนเขียนระบบที่มี audit log และตัวปิดบังความลับ
ทางแก้แบบสั้น
ปิดบังก่อนตัดความยาว ไม่ใช่ตัดก่อน และเขียนเทสต์ที่วนทุกหน้าต่างความยาวแล้วตรวจทุกชิ้นส่วนของ token
ข้ามไปขั้นตอน

พอดีผมให้ agent อีกตัวหนึ่งมาอ่านโค้ดส่วนพอร์ตคำสั่งแบบจับผิด หลังจากที่ผมเขียนตัวปิดบัง token ใน log เสร็จและมีเทสต์ยืนยันว่า "ใน log ไม่มี token" ผ่านเขียวเรียบร้อยแล้ว ผู้ตรวจชี้ว่าลำดับผิด ข้อความถูกตัดความยาวก่อนจะถูกปิดบัง และถ้าผู้เรียกใส่ token มาผิดช่อง ครึ่งหนึ่งของ token จะลงไฟล์ ขึ้น endpoint สถานะ และขึ้นจอ โดยที่เทสต์ของผมยังเขียวอยู่เหมือนเดิม

ระบบนี้คือโปรแกรมหน้าจอมอนิเตอร์ข้อมูลเวลาจริงที่ผมเขียนด้วย C# กับเอนจินซึ่งเป็น process ลูก ชื่อเครื่องมือ ชื่อผู้เรียก และค่า token ในบทความเป็นของสมมุติทั้งหมด โค้ดผมเขียนย่อใหม่เป็น C# ให้อ่านง่าย ส่วนลำดับเหตุการณ์กับตัวเลขเป็นของจริง

ระบบที่เกี่ยวข้อง: พอร์ตคำสั่ง audit log และป้ายผู้เรียกล่าสุด

เอนจินเปิดพอร์ตคำสั่งบน loopback ให้เครื่องมือ AI เรียกใช้ ด่านหน้าพอร์ตนี้มีครบตามตำรา คือ token, allow-list ของเครื่องมือที่เรียกได้, rate limit, idempotency key และ audit log แบบ jsonl ที่บันทึกทุกคำขอ แต่ละบรรทัดเก็บ caller ซึ่งเป็นชื่อที่ผู้เรียกบอกมาเอง ชื่อเครื่องมือ สถานะ และเวลา

caller ล่าสุดไม่ได้อยู่แค่ในไฟล์ มันถูกส่งต่อไปอีกสองที่ ที่แรกคือ endpoint สถานะ ซึ่งอยู่อีกพอร์ตหนึ่งและไม่ต้องใช้ token ที่สองคือป้ายบนหน้าจอที่บอกว่าใครเรียกเข้ามาล่าสุด

ทางเดินของช่อง callerคำขอเข้าcaller = ชื่อที่บอกเอง->audit logไฟล์ jsonl->endpoint สถานะไม่ต้องใช้ token->ป้ายบนหน้าจอใครเดินผ่านก็เห็น

ช่อง caller เป็นข้อความอิสระที่ฝั่งผู้เรียกเป็นคนกำหนด ผมเลยตัดความยาวไว้ที่ 32 ตัวอักษร ซึ่งเป็นเรื่องปกติของช่องข้อความอิสระที่จะถูกเขียนลง log และมีตัว redact ที่ค้น token ในข้อความแล้วแทนด้วยดอกจัน ทั้งสองอย่างดูสมเหตุสมผลเมื่อมองแยกกัน

บั๊ก: ตัดก่อน แล้วค่อยปิดบัง

โค้ดรุ่นที่มีปัญหาหน้าตาประมาณนี้

// รุ่นที่มีบั๊ก
static string Clip(string s, int max) => s.Length <= max ? s : s.Substring(0, max);

static string Redact(string s, string token) => s.Replace(token, "****");   // ค้นทั้งก้อน แบบ exact

static AuditEntry Build(string caller, string tool, string status, string token) => new AuditEntry
{
    Caller = Redact(Clip(caller, 32), token),      // ตัดเหลือ 32 ก่อน แล้วค่อยส่งเข้า redact
    Tool = tool,
    Status = status,
    At = DateTimeOffset.UtcNow,
};

token ของระบบเป็น hex 64 ตัว ถ้าผู้เรียกใส่ token มาเป็นชื่อ caller ซึ่งเกิดได้จริงเวลา config ของฝั่งผู้เรียกสลับช่องกัน ลำดับที่เกิดคือ Clip ตัดเหลือ hex 31-32 ตัวแรกของ token จากนั้น Redact เอา token เต็ม 64 ตัวไปค้นในข้อความที่ยาวแค่ครึ่งเดียว ซึ่งไม่มีวันเจอ ไม่มีอะไรถูกแทนที่ ข้อความครึ่งก้อนนั้นเลยเดินทางต่อไปครบทุกปลายทาง

{"at":"2026-10-01T09:00:00Z","caller":"0123456789abcdef0123456789abcde","tool":"get_status","status":"ok"}

บรรทัดข้างบนคือหน้าตาของสิ่งที่ลงไฟล์ (ค่าเป็นของสมมุติ) hex 31 ตัวของ token อยู่ในไฟล์ audit อยู่ใน endpoint สถานะที่ process ไหนในเครื่องก็ GET ได้โดยไม่ต้องยืนยันตัว และอยู่บนป้ายหน้าจอ

ลำดับผิด: ตัดก่อน แล้วค่อยปิดบังcallertoken 64 ตัวตัดเหลือ 32เหลือครึ่งก้อนredactหาก้อน 64 ตัว ไม่เจอaudit loghex 31 ตัวหลุดลำดับถูก: ปิดบังก่อน แล้วค่อยตัดcallertoken 64 ตัวredactลบช่วง hex ที่ตรงตัดเหลือ 32ตัดของที่สะอาดแล้วaudit log****หน้าต่างเลื่อน 16 ตัวบน token 64 ตัว16 ตัวตัวที่ 1ตัวที่ 6464 - 16 + 1 = 49 หน้าต่าง ช่วง hex ยาว 16 ขึ้นไปที่ตรงหน้าต่างใดก็ตาม ถูกลบทั้งช่วง
แถวบนคือของเดิม ตัวตัดความยาวทำลายรูปร่างที่ตัว redact ใช้ค้นหา แถวล่างสลับลำดับ ส่วนแถบล่างสุดคือวิธีที่ตัว redact รุ่นใหม่จำ token ได้แม้เหลือมาแค่บางส่วน

ทำไมเทสต์เดิมถึงเขียว

เทสต์ที่ผมภูมิใจว่ามีอยู่แล้วเขียนไว้แบบนี้

// เทสต์เดิม: ผ่านเขียวมาตลอด และพิสูจน์ผิดสิ่ง
[Fact]
public void AuditLogDoesNotContainToken()
{
    string tok = SampleToken();
    string log = RunRequestAndReadAuditLog(caller: tok, tool: "get_status");
    Assert.DoesNotContain(tok, log);          // ถามว่า "มี token ทั้งก้อนไหม" คำตอบคือไม่มี จริง ๆ
}

คำยืนยันในเทสต์เป็นจริงทุกตัวอักษร ใน log ไม่มี token ทั้งก้อนอยู่จริง ๆ มีแค่ครึ่งก้อน เทสต์ตัวนี้ใช้เงื่อนไขเดียวกับตัว redact คือมองหาก้อนเต็ม มันเลยมีจุดบอดเดียวกันเป๊ะ บั๊กที่ทำให้ตัว redact พลาดก็ทำให้เทสต์พลาดไปพร้อมกัน เทสต์ที่ใช้ตรรกะเดียวกับโค้ดที่มันตรวจ ไม่ได้ตรวจอะไรเพิ่มเลยครับ

เทสต์ความลับต้องหาชิ้นส่วน ไม่ใช่ทั้งก้อน

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

เดาผิดรอบแรก: คิดว่าย้ายลำดับแล้วจบ

พอเห็นบั๊ก ผมคิดว่าแก้บรรทัดเดียวก็พอ สลับเป็น Clip(Redact(caller, token), 32) แล้วปิดงาน แต่พอไล่ดูทุกจุดที่ข้อความจากภายนอกวิ่งเข้า log ก็เจอว่ายังมีทางหลุดอีกอย่างน้อยสองทาง

ทางแรก body ของคำขอถูกตัดที่ 64 KB ก่อนส่งเข้า redact เหมือนกัน เป็นลำดับผิดแบบเดียวกันในอีกที่หนึ่ง ถ้า token ไปคร่อมอยู่ตรงรอยตัดพอดี ส่วนหน้าของมันจะรอด

ทางที่สอง ตัว redact ค้นแบบ exact ถ้า token ถูกส่งมาเป็น hex ตัวพิมพ์ใหญ่ มันก็ไม่ตรงกับ token ตัวพิมพ์เล็กที่เก็บไว้ ทั้งที่เป็นความลับตัวเดียวกันทุกบิต

สองข้อนี้บอกว่าปัญหาไม่ใช่แค่ลำดับ ตัว redact เองก็เปราะเกินไป มันรู้จักความลับแค่รูปเดียว คือรูปที่ตรงกับที่เก็บไว้ทุกตัวอักษร

ทางแก้ที่ลงจริงสามข้อ

  1. ทุกสตริงผ่าน redact ก่อนการตัดความยาวเสมอ ทั้ง caller และ body ไม่มีข้อยกเว้น ตัวตัดความยาวจะได้เห็นแต่ข้อความที่สะอาดแล้ว
  2. ตัว redact ลบเป็นช่วง ไม่ใช่เป็นก้อน มันหาช่วง hex ต่อเนื่องยาว 16 ตัวขึ้นไป แล้วถ้าหน้าต่าง 16 ตัวใดก็ตามในช่วงนั้นตรงกับส่วนใดของ token ให้ลบทั้งช่วง ไม่สนตัวพิมพ์
  3. เทสต์วนทุกหน้าต่าง 16 ตัวของ token แล้วยืนยันว่าไม่มีหน้าต่างไหนอยู่ใน log ใน endpoint สถานะ หรือในข้อความที่ส่งกลับผู้เรียก

ตัว redact รุ่นใหม่เขียนได้ไม่ยาว

public sealed class Redactor
{
    private const int Win = 16;
    private readonly HashSet<string> _windows = new(StringComparer.Ordinal);

    // รับความลับได้หลายตัว: token กลาง, credential ของหน้าจอ ฯลฯ
    public Redactor(params string[] secrets)
    {
        foreach (string s in secrets)
        {
            string t = s.ToLowerInvariant();
            for (int i = 0; i + Win <= t.Length; i++) _windows.Add(t.Substring(i, Win));
        }
    }

    public string Redact(string s)
    {
        if (string.IsNullOrEmpty(s)) return s;
        var sb = new StringBuilder(s.Length);
        int pos = 0;
        while (pos < s.Length)
        {
            if (!Uri.IsHexDigit(s[pos])) { sb.Append(s[pos++]); continue; }
            int end = pos;
            while (end < s.Length && Uri.IsHexDigit(s[end])) end++;      // ช่วง hex ต่อเนื่อง [pos, end)
            string run = s.Substring(pos, end - pos);
            sb.Append(Hits(run) ? "****" : run);
            pos = end;
        }
        return sb.ToString();
    }

    private bool Hits(string run)
    {
        if (run.Length < Win) return false;
        string r = run.ToLowerInvariant();
        for (int i = 0; i + Win <= r.Length; i++)
            if (_windows.Contains(r.Substring(i, Win))) return true;
        return false;
    }
}

และจุดเรียกใช้เหลือลำดับเดียวที่ถูก

static string Clip(string s, int max) => s.Length <= max ? s : s.Substring(0, max);

entry.Caller = Clip(redactor.Redact(caller), 32);            // ลำดับนี้เท่านั้น
entry.Body   = Clip(redactor.Redact(body), 64 * 1024);       // body ก็เหมือนกัน

ทำไมต้อง 16 ตัว ตัวเลขนี้เป็นการแลกกันระหว่างสองความเสี่ยง ถ้าหน้าต่างสั้นเกินไป เช่น 4 ตัว ข้อความธรรมดาที่บังเอิญมี hex สี่ตัวตรงกับส่วนหนึ่งของ token จะโดนลบไปด้วย log จะเต็มไปด้วยดอกจันจนอ่านไม่ออก ถ้ายาวเกินไป ชิ้นส่วนที่สั้นกว่าหน้าต่างจะหลุดได้ hex 16 ตัวคือ 64 บิต โอกาสที่ข้อความปกติจะตรงโดยบังเอิญแทบไม่มี และชิ้นส่วนที่สั้นกว่า 16 ตัวเปิดเผย token ได้ไม่ถึงหนึ่งในสี่ของก้อน

ขอบเขตที่ตัว redact แบบนี้ยังมองไม่เห็น

มันจำได้เฉพาะความลับที่ยังอยู่ในรูป hex ต่อเนื่อง ถ้ามีอะไรมาคั่นกลางทุกไม่กี่ตัว เช่น ขีดหรือช่องว่าง หรือ token ถูกเข้ารหัสเป็นรูปอื่นไปแล้วอย่าง base64 ช่วง hex จะขาดหรือหายไป และไม่มีหน้าต่างไหนตรง ตัว redact คือด่านสุดท้าย ไม่ใช่ด่านเดียว ด่านแรกยังต้องเป็นการไม่เอาความลับใส่ลงในช่องที่จะถูก log ตั้งแต่ต้น

เทสต์ใหม่: วนทุกหน้าต่าง ตรวจทุกปลายทาง

เทสต์รุ่นใหม่ต่างจากรุ่นเก่าสองเรื่อง มันถามหาชิ้นส่วนแทนก้อน และมันตรวจทุกที่ที่ caller เดินทางไปถึง ไม่ใช่แค่ไฟล์ log

private static string SampleToken() => string.Concat(Enumerable.Repeat("0123456789abcdef", 4));   // 64 ตัว ของสมมุติ

public static IEnumerable<object[]> Shapes()
{
    string tok = SampleToken();
    yield return new object[] { tok };                               // ทั้งก้อน
    yield return new object[] { tok.ToUpperInvariant() };            // ตัวพิมพ์ใหญ่
    yield return new object[] { "agent-" + tok };                    // มีคำนำหน้า รอยตัดเลื่อน
    yield return new object[] { tok.Substring(20) + tok };           // ชิ้นส่วนต่อกับก้อนเต็ม
}

[Theory]
[MemberData(nameof(Shapes))]
public void NoWindowOfTheTokenSurvivesAnywhere(string caller)
{
    string tok = SampleToken();
    using var h = TestHost.Start(tok);

    string reply  = h.Call(caller: caller, tool: "get_status");
    string audit  = h.ReadAuditLog();
    string status = h.GetStatusEndpoint();                           // endpoint ที่ไม่ต้องใช้ token

    foreach (string sink in new[] { audit, status, reply })
        for (int i = 0; i + 16 <= tok.Length; i++)
            Assert.DoesNotContain(tok.Substring(i, 16), sink, StringComparison.OrdinalIgnoreCase);
}

token 64 ตัวมีหน้าต่าง 16 ตัวทั้งหมด 49 หน้าต่าง คูณสามปลายทาง คูณจำนวนรูปแบบของ caller ที่ป้อนเข้าไป กับโค้ดรุ่นเก่า เทสต์แบบนี้ต้องล้ม เพราะ hex 31 ตัวที่หลุดมีหน้าต่าง 16 ตัวของ token อยู่ข้างในถึง 16 หน้าต่าง ซึ่งเป็นสิ่งที่ผมควรได้เห็นตั้งแต่วันแรก ข้อดีอีกอย่างคือเทสต์ไม่ผูกกับวิธีทำงานของตัว redact แล้ว ต่อให้วันหน้าเปลี่ยนตัว redact เป็นวิธีอื่นทั้งหมด คำถามของเทสต์ก็ยังเป็นคำถามเดิม คือมีชิ้นส่วนของความลับโผล่ที่ปลายทางไหนบ้างไหม

เทสต์เดิม

ไม่มี token ทั้งก้อน

  • ใช้เงื่อนไขเดียวกับตัว redact
  • ตรวจไฟล์ log ที่เดียว
  • ป้อนรูปแบบเดียว
  • เขียวทั้งที่ hex 31 ตัวหลุด
เทสต์ใหม่

ไม่มีหน้าต่าง 16 ตัวใดเลย

  • 49 หน้าต่างต่อ token หนึ่งตัว
  • ตรวจ log, endpoint สถานะ และคำตอบ
  • ป้อนหลายรูปแบบ รวมตัวพิมพ์ใหญ่
  • ไม่ขึ้นกับวิธีทำงานของตัว redact

เรื่องข้างเคียงสามเรื่องจากการตรวจรอบเดียวกัน

ผู้ตรวจไม่ได้เจอแค่เรื่องลำดับ ยังมีอีกสามเรื่องที่อยู่ในตระกูลเดียวกัน คือความลับกับด่านหน้าพอร์ตที่ดูถูกต้องบนกระดาษ

ความลับที่เครื่องมือเขียนลงไฟล์ config ซึ่ง git ติดตาม token ถูกเครื่องมือสร้างแล้วเขียนลงไฟล์ config ไฟล์เดียวกับที่อยู่ใน repo ถ้าไม่กันไว้ คำสั่ง commit แบบเหมารวมครั้งถัดไปจะพา token ขึ้น repo ไปด้วย ผมเลยกันค่านี้ออกจาก commit โดยเจตนา ใน index เก็บค่า null ไว้ เวลาตรวจต้องดูของใน index ไม่ใช่ของใน working tree เช่นแบบนี้

# ดูค่าที่อยู่ใน index (ของที่จะถูก commit) ไม่ใช่ไฟล์บนดิสก์
git show :configs/profile.json | grep -n '"token"'
#   12:  "token": null,

# กวาดทั้ง index หา hex ยาว 64 ตัวที่ไม่ควรอยู่ตรงนั้น
git grep --cached -nE '\b[0-9a-f]{64}\b' -- configs/ && echo "[BLOCK] มี hex 64 ตัวใน index" || echo "[OK]"

lockout ต่อ IP ใช้ไม่ได้กับพอร์ต loopback กติกา "ผิด 5 ครั้งในหนึ่งนาที ล็อก IP นั้น" เป็นกติกาที่ดีบนพอร์ตสาธารณะ แต่บน loopback ผู้เรียกทุกตัวคือ 127.0.0.1 เหมือนกันหมด เครื่องมือตัวหนึ่งพิมพ์ token ผิด ผู้เรียกทุกตัวในเครื่องโดนล็อกไปพร้อมกัน รวมถึงตัวที่ทำถูกทุกอย่าง กลไกที่ยกมาจากบริบทอื่นต้องถามก่อนเสมอว่ากุญแจที่มันใช้แยกผู้เรียก ยังแยกผู้เรียกได้อยู่ไหมในบริบทใหม่

ทางลัดของหน้าจอเราเองต้องมี credential แยก หน้าจอควบคุมต้องเรียกบางอย่างที่อยู่นอก allow-list ของเครื่องมือ AI เส้นทางที่ข้าม allow-list ให้หน้าจอของเราเองเลยต้องมี credential ของมันเอง สุ่มใหม่ทุกครั้งที่เอนจินสตาร์ต แล้วเขียนลงไฟล์ที่หน้าจออ่านได้ ไม่ใช่ใช้ token กลางร่วมกับ AI เพราะถ้าใช้ร่วมกัน ใครถือ token ของ AI ก็ข้าม allow-list ได้ทันที และค่านี้ต้องถูกส่งเข้าตัว redact ด้วย ไม่งั้น debug log จะกลายเป็นทางรั่วใหม่

// ตอนเอนจินสตาร์ต: credential ของหน้าจอ สุ่มใหม่ทุกรอบ อายุเท่ากับ process นี้
string uiKey = Convert.ToHexString(RandomNumberGenerator.GetBytes(32)).ToLowerInvariant();
File.WriteAllText(Path.Combine(stateDir, "ui.key"), uiKey);

// ตัว redact ต้องรู้จักความลับทุกตัวที่ process นี้ถืออยู่ ไม่ใช่แค่ตัวแรกที่นึกถึง
var redactor = new Redactor(apiSecret, uiKey);

เรื่อง log ที่เป็นทางรั่วของข้อมูลอีกแบบหนึ่ง คือ log ที่สิทธิ์ไฟล์หลวมแล้วถูกอัปออกนอกเครื่องพร้อม backup ผมเขียนแยกไว้ใน บทความเรื่อง log 0644 ที่ถูกอัปออกทุกคืน สองเรื่องนี้เสริมกัน อันนั้นคือ log ไปอยู่ผิดที่ อันนี้คือของที่ไม่ควรอยู่ใน log ตั้งแต่แรก

ตัวเลขและบทเรียน

64 ตัวความยาว token แบบ hex
32 ตัวความยาวที่ช่อง caller ถูกตัด
31 ตัวhex ที่หลุดลง log
3 ที่ไฟล์ audit, endpoint สถานะ, หน้าจอ
64 KBจุดตัดของ body ที่ผิดลำดับเหมือนกัน
16 ตัวขนาดหน้าต่างของตัว redact
49หน้าต่างต่อ token ที่เทสต์วนตรวจ
5 ครั้ง/นาทีlockout ที่ใช้กับ loopback ไม่ได้

ต้องบอกขนาดของเรื่องให้ตรงด้วยครับ hex 31 ตัวคือ 124 บิตจาก 256 บิต ส่วนที่เหลืออีก 132 บิตยังเดาไม่ได้ในทางปฏิบัติ งานนี้เลยไม่ใช่เหตุการณ์ที่ token ใช้การไม่ได้แล้ว แต่มันคือหลักฐานว่ากติกา "ความลับไม่ลง log" ของผมไม่ได้เป็นจริงอย่างที่เทสต์บอก และถ้า token สั้นกว่านี้ หรือช่องนั้นถูกตัดยาวกว่านี้ ผลจะต่างกันมาก

บทเรียนสั้น ๆ ที่ผมเอาไปใช้กับทุกที่ที่มีการ log ข้อความจากภายนอก คือ redact ก่อน truncate เสมอ การแปลงข้อความทุกชนิดต้องเกิดหลังการปิดบัง ไม่ใช่ก่อน และเทสต์ต้องหาชิ้นส่วนของความลับที่ทุกปลายทาง ไม่ใช่หาทั้งก้อนที่ปลายทางเดียว อีกข้อคือการให้คนอื่นหรือ agent อีกตัวอ่านโค้ดแบบจับผิดคุ้มค่ามากกับโค้ดส่วนความปลอดภัย เพราะคนเขียนกับเทสต์ของคนเขียนมักมีจุดบอดเดียวกัน ใครจะเอาตัว redact กับเทสต์ข้างบนไปใช้ก็เปลี่ยนขนาดหน้าต่างตามความยาว token ของตัวเองได้ตามสะดวกครับ ส่วนเคสที่ token ถูกแปลงเป็นรูปอื่นก่อนถึง log ซึ่งตัวนี้ยังจับไม่ได้ เดี๋ยวกลับมาแก้เพิ่มให้ภายหลังครับ

อยากให้เราทำให้ไหม?

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

คุยเรื่องงานกับเรา

เคยเจองานแบบนี้ไหม? ติดตรงไหน?

มาแลกวิธีกันต่อในบอร์ด LinuxThai

→ คุยต่อในบอร์ด

ความคิดเห็น 0

ยังไม่มีความคิดเห็น — เป็นคนแรกเลย