เข้ารหัส Secret ใน Git

หลายๆ คนน่าจะทราบดีว่าเราไม่ควร commit รหัสลับต่างๆ ลงไปใน Git เพื่อป้องกันไม่ให้รหัสลับนั้นหลุดออกไป

แต่จากประสบการณ์พบว่า หลายๆ ครั้งแล้วเราก็ยังจะจำเป็นต้องมี Git Repo อีกอันหนึ่งที่รวบรวมรหัสลับทั้งหมดไว้อยู่ดี หลักๆ ก็คือใช้ในการ Deploy เพื่อให้แชร์รหัสกับคนในทีมได้ ซึ่งวิธีการที่มักใช้กันอยู่ก็จะมีหลายๆ ท่า

  • ถ้าใช้ GitLab ทำ CD ท่าที่จะใช้กันก็คือใช้ Secret variable
  • ถ้าใช้ Travis ก็สามารถให้ command line มัน encrypt secret ได้
  • หรือ Ansible ก็มี Ansible Vault

แล้วถ้าเราไม่ได้ใช้ระบบพวกนี้เลยล่ะ? วิธีที่เหมาะก็คงจะต้อง encrypt file นั้นตรงๆ ซึ่งก็มีโปรแกรมตัวช่วยหลายตัวสำหรับ Git ไม่ว่าจะเป็น Blackbox ของ Stack Exchange, git-secret

สำหรับตัวที่จะแนะนำในวันนี้เป็นตัวที่ลองแล้วคิดว่าใช้งานง่ายที่สุด นั่นคือ git-crypt แต่อาจจะติดตั้งยากเล็กน้อยเพราะต้องคอมไพล์

git-crypt

ก่อนอื่นการติดตั้ง ถ้าใช้ Arch Linux ก็สามารถไปโหลดจาก AUR ได้เลย ถ้าไม่ได้ใช้ก็คอมไพล์เองได้ไม่ยาก

ถัดมาให้เราเปิดใช้ git-crypt ด้วยการรัน git-crypt init ใน repo

$ git-crypt init
Generating key...

ถัดมาเราจะต้องแนะนำตัวให้ git-crypt รู้จักโดยใช้ PGP key ของเรา ถ้ายังไม่มีก็ generate เสียก่อน

$ gpg --gen-key
gpg (GnuPG) 2.2.1; Copyright (C) 2017 Free Software Foundation, Inc.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Note: Use "gpg --full-generate-key" for a full featured key generation dialog.

GnuPG needs to construct a user ID to identify your key.

Real name: กรอกชื่อ
Email address: กรอกอีเมล
You selected this USER-ID:
    "Test key <test@example.com>"

Change (N)ame, (E)mail, or (O)kay/(Q)uit? o
We need to generate a lot of random bytes. It is a good idea to perform
some other action (type on the keyboard, move the mouse, utilize the
disks) during the prime generation; this gives the random number
generator a better chance to gain enough entropy.
We need to generate a lot of random bytes. It is a good idea to perform
some other action (type on the keyboard, move the mouse, utilize the
disks) during the prime generation; this gives the random number
generator a better chance to gain enough entropy.
gpg: key C0E4A829EAD6456C marked as ultimately trusted
gpg: revocation certificate stored as '/home/whs/.gnupg/openpgp-revocs.d/9C765CE23C4E31A9BE50BF25C0E4A829EAD6456C.rev'
public and secret key created and signed.

pub   rsa2048 2017-09-21 [SC] [expires: 2019-09-21]
      9C765CE23C4E31A9BE50BF25C0E4A829EAD6456C
uid                      Test key <test@example.com>
sub   rsa2048 2017-09-21 [E] [expires: 2019-09-21]

จะเห็นเลขฐาน 16 ยาวๆ 9C765CE23C4E31A9BE50BF25C0E4A829EAD6456C ตรงนี้คือ Key ID เต็มของเรา ซึ่งจะเรียกด้วยชื่อย่อ 8 ตัวหลังก็ได้ ก็คือ EAD6456C (ในตัวอย่างต่อๆ ไปจะใช้ key ผมคือ 5AD1E4A5)

พอมี key แล้ว ก็ add ตัวเราเข้าไปใน git-crypt

$ git-crypt add-gpg-user 5AD1E4A5
[master 8c02850] Add 1 git-crypt collaborator
 2 files changed, 3 insertions(+)
 create mode 100644 .git-crypt/.gitattributes
 create mode 100644 .git-crypt/keys/default/0/114F08A7E2081ACA01EC7738461CCB345AD1E4A5.gpg

ตรงนี้จะใส่เป็น Key ID เต็ม หรือย่อก็ได้ หรือแม้แต่กรอกอีเมลที่ใช้สร้าง key ก็ได้เช่นกัน

ถัดมาเราจะสร้างไฟล์ .gitattributes ขึ้นมา ไฟล์นี้จะบอกว่าไฟล์ไหนบ้างที่จะ encrypt

dns/service-account.json filter=git-crypt diff=git-crypt
**/secrets.yaml filter=git-crypt diff=git-crypt

จากตัวอย่างก็คือให้เข้ารหัสไฟล์ dns/service-account.json และไฟล์ secrets.yaml ในทุกๆ folder เมื่อเขียนเสร็จแล้วก็ให้ add ไฟล์ทั้งหมดลงไปใน Git แล้ว commit ได้เลย

ทดสอบ Encryption

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

วิธีแรกคือเช็ค status ด้วยคำสั่ง git-crypt status จะได้ผลลัพท์ประมาณนี้

$ git-crypt status
not encrypted: README.md
not encrypted: .git-crypt/.gitattributes
not encrypted: .git-crypt/keys/default/0/114F08A7E2081ACA01EC7738461CCB345AD1E4A5.gpg
not encrypted: .gitattributes
    encrypted: dns/service-account.json
    encrypted: kube/zipkin/secrets.yaml

แบบนี้ก็แสดงว่าเราได้ระบุการเข้ารหัสไฟล์ถูกต้องแล้ว ถ้าไฟล์ไหนที่ตั้งใจจะเข้ารหัสแต่ไม่ขึ้นว่า encrypted ก็ควรจะดู glob ใน .gitattributes ใหม่

ถัดมาเราจะทดสอบให้ git-crypt ล็อค repo เรา ด้วยคำสั่ง git-crypt lock เมื่อสั่งแล้วจะไม่ได้ผลลัพท์อะไร แต่ถ้าเปิดดูจะเห็นว่าไฟล์ข้างในจะอ่านไม่ได้ ต้องสั่ง git-crypt unlock เพื่อปลดล็อค

เข้ารหัสไฟล์เก่า

ถ้ามีไฟล์ secret ที่ไม่ได้เข้ารหัสอยู่แล้ว แล้วเพิ่ม git-crypt ไปทีหลังจะพบว่ามันไม่เข้ารหัสให้ วิธีการก็คือให้สั่ง git-crypt status -f เพื่อให้มัน add เวอร์ชั่นที่ encrypt แล้วเข้าไป เสร็จแล้วก็ Commit

$ git-crypt status -f
kube/zipkin/secrets.yaml: staged encrypted version
Staged 1 encrypted files.
Warning: if these files were previously committed, unencrypted versions still exist in the repository's history.
$ git commit -m "Encrypted secret"

ปัญหาคือ ไฟล์นี้เคยมีอยู่ใน version เก่าๆ แล้ว เราเลยจะต้องใช้โปรแกรมเข้ามาแก้ History ของ Git ลบไฟล์ออก

คำเตือน: การแก้ Git History จะต้องทำ Force push/pull อาจจะมีปัญหากับคนที่ใช้ Repo ร่วมกัน ควรแจ้งให้ทุกคนทราบก่อน

โปรแกรมที่เราจะใช้แก้ก็คือ BFG Repo Cleaner ซึ่งเป็น Java โหลดได้จากในหน้าเว็บเลย วิธีการใช้งานก็ไม่ยาก

$ java -jar ~/Downloads/bfg-1.12.15.jar -D service-account.json    

Using repo : /home/whs/apps/repo/.git

Found 42 objects to protect
Found 4 commit-pointing refs : HEAD, refs/heads/master, refs/remotes/origin/master, refs/stash

Protected commits
-----------------

These are your protected commits, and so their contents will NOT be altered:

 * commit 5fdf16ac (protected by 'HEAD') - contains 1 dirty file : 
    - dns/service-account.json (2.3 KB)

WARNING: The dirty content above may be removed from other commits, but as
the *protected* commits still use it, it will STILL exist in your repository.

Details of protected dirty content have been recorded here :

/home/whs/apps/repo.bfg-report/2017-09-21/22-42-31/protected-dirt/

If you *really* want this content gone, make a manual commit that removes it,
and then run the BFG on a fresh copy of your repo.


Cleaning
--------

Found 72 commits
Cleaning commits:       100% (72/72)
Cleaning commits completed in 278 ms.

Updating 3 Refs
---------------

    Ref                          Before     After   
    ------------------------------------------------
    refs/heads/master          | 5fdf16ac | c62f12f6
    refs/remotes/origin/master | 4946f48a | b83dcc06
    refs/stash                 | ecafbd96 | 56772cea

Updating references:    100% (3/3)
...Ref update completed in 27 ms.

Commit Tree-Dirt History
------------------------

    Earliest                                              Latest
    |                                                          |
    DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDm

    D = dirty commits (file tree fixed)
    m = modified commits (commit message or parents changed)
    . = clean commits (no changes to file tree)

                            Before     After   
    -------------------------------------------
    First modified commit | 2c741fa1 | 0f4fbd88
    Last dirty commit     | ecafbd96 | 56772cea

Deleted files
-------------

    Filename               Git id           
    ----------------------------------------
    service-account.json | 84044ad7 (2.3 KB)


In total, 146 object ids were changed. Full details are logged here:

    /home/whs/apps/repo.bfg-report/2017-09-21/22-42-31

BFG run is complete! When ready, run: git reflog expire --expire=now --all && git gc --prune=now --aggressive

ตัวเลือก -D จะให้เราระบุชื่อไฟล์ที่ต้องการลบ โดยไม่สามารถใส่ path ได้ (ถ้ามีไฟล์ชื่อเดียวกันหลายๆ ที่ก็จะถูกลบทั้งหมด)

ถ้าอ่านใน Log ก็จะเห็นว่า BFG จะไม่แก้ไข commit ล่าสุดของเรา ฉะนั้นไฟล์ที่เราเพิ่ง encrypt + commit ไปจะไม่หาย ส่วนไฟล์นี้ใน history จะถูกลบออกทั้งหมด เมื่อพร้อมแล้วก็ให้ลบขยะออกจาก Git ถาวรด้วยคำสั่ง git reflog expire --expire=now --all && git gc --prune=now --aggressive ที่เค้าให้มา แล้วก็ Force push ได้เลย

Backup GPG key

สุดท้ายเพื่อความปลอดภัย ควรจะ backup private key ที่เราใช้ด้วย

$ gpg -a --export-secret-keys 5AD1E4A5 > 5AD1E4A5.key

(สามารถกรอก key ID เป็นแบบสั้น แบบยาวหรืออีเมลก็ได้เหมือนเดิม)

เมื่อได้ไฟล์มาแล้วก็ควรเก็บไว้ที่ๆ ปลอดภัย เช่นที่ผมใช้คือถ้าจะเอา key นี้ไปเก็บบน cloud storage ใดๆ ก็จะต้อง encrypt ด้วยรหัสผ่านอีกครั้งหนึ่งที่คนละรหัสกับรหัสเข้า storage นั้นๆ หรือเลี่ยงได้ก็จะไม่ใช้ ก๊อปไฟล์ระหว่างเครื่องเองอย่างเดียว

ลองเล่น Stackdriver Trace บน Django

หลังจาก implement AWS X-Ray ให้ที่บริษัทไป ผมก็เลยว่าอยากจะ implement ระบบคล้ายๆ กันให้ TipMe บ้าง แต่ TipMe อยู่บน Google Cloud ก็เลยจะต้องใช้บริการของเค้าคือ Stackdriver Trace

(Note: ถ้ายังไม่ได้อ่านตอนของ X-Ray แนะนำให้อ่านก่อนเพื่อเข้าใจ concept ของ Tracing ครับ)

Zipkin

สำหรับการใช้งาน Stackdriver Trace นั้นจะแตกต่างกับของ X-Ray ตรงที่ Trace มี API ให้เราเลือกใช้ถึง 3 แบบ คือ

  1. REST API ซึ่งจะเป็น API เฉพาะของ Trace เอง เช่นเดียวกับของ X-Ray
  2. gRPC API เป็น custom API เช่นกัน แต่ตอนนี้ยังไม่มี client library ให้ใช้
  3. Zipkin API ซึ่ง Zipkin เป็นโปรแกรมสำหรับทำ tracing แบบ open source แล้ว Google Cloud นำมาแก้ให้ใช้ gRPC API ของเค้าเป็น backend อีกทีหนึ่ง (จะเรียกว่าเป็น adapter ก็ได้)

เพื่อไม่ให้ vendor lock in เราก็จะเลือกใช้ Zipkin API ฉะนั้นอย่างแรกที่จะต้องทำคือติดตั้ง Zipkin Collector ของเค้าก่อน สำหรับบน Kubernetes ก็ใช้ Deployment ประมาณนี้

apiVersion: v1
kind: ReplicationController
metadata:
  name: zipkin
spec:
  replicas: 1
  selector:
    app: zipkin
  template:
    metadata:
      labels:
        app: zipkin
    spec:
      containers:
      - name: zipkin
        image: gcr.io/stackdriver-trace-docker/zipkin-collector
        ports:
        - containerPort: 9411
        env:
        - name: GOOGLE_APPLICATION_CREDENTIALS
          value: /secrets/service-account
        livenessProbe:
          httpGet:
            path: /health
            port: 9411
          initialDelaySeconds: 120
        readinessProbe:
          httpGet:
            path: /health
            port: 9411
        resources:
          requests:
            memory: "0Mi"
            cpu: "3m"
          limits:
            memory: "256Mi"
        volumeMounts:
        - name: secret
          mountPath: /secrets
          readOnly: true
      volumes:
      - name: secret
        secret:
          secretName: zipkin
---
apiVersion: v1
kind: Service
metadata:
  name: zipkin
  labels:
    app: zipkin
spec:
  selector:
    app: zipkin
  ports:
    - port: 9411
---
apiVersion: v1
kind: Secret
metadata:
  name: zipkin
type: Opaque
data:
  service-account: แปะ service account เป็น base64

เนื่องจากบน Kubernetes เราจะใช้ instance role ไม่ได้ (เพราะไม่ได้เซตสิทธิ์ไว้ตอนเปิด cluster) ก็เลยจะต้องใช้ Service account แทน วิธีการสร้าง Service account ก็คือ

  1. เข้าไปที่ Service Accounts
  2. กด Create service account ด้านบน
  3. กรอก Service account name ตามชอบ และเลือก Role เป็น Cloud Trace > Cloud Trace Agent
  4. ติ๊ก Furnish a new private key และเลือก JSON
  5. กด Create
  6. เมื่อได้ JSON มา ให้เอาไปเข้ารหัส Base64 แล้วแปะใน Secret ด้านบน (cat file.json | base64 -w0)

พอเสร็จแล้วก็ kubectl apply -f zipkin.yaml เป็นอันเรียบร้อย

Integrate กับ Django

ถัดมาเราจะต้องติดตั้ง Tracer ลงในโปรแกรมของเรา ซึ่งถ้าเป็น Node.js แบบที่วงในก็คงจะง่าย แต่พอเป็น Python แล้วก็พบว่าเราคงจะต้องทำเอง -_-!! ซึ่งของที่ผมออกแรงนั่งทำมาก็ปล่อยเป็น open source แล้วครับ ฟีเจอร์หลักๆ ก็คือ

  • Trace request ได้
  • Trace request ออกไปทาง urllib3 (request module) ได้ รวมทั้งจะแปะ X-B3-TraceId ส่งไปให้ด้วย
  • Trace template render time ได้
  • Trace database query ได้ (แต่จะไม่ log parameter ของ query เพื่อความปลอดภัย)

วิธีการติดตั้งก็คือ

  1. เพิ่ม django-zipkin-trace==1.0.0 ลงใน requirements.txt
  2. pip install -r requirements.txt
  3. แก้ไข settings.py ของเราในส่วนของ MIDDLEWARES เติม zipkin_trace.ZipkinMiddleware ไปเป็นอันบนสุด
  4. เพิ่ม ZIPKIN_SERVER='http://zipkin:9411' ลงใน settings.py

ถ้าอยากลองเทส ก็สามารถเทสบนเครื่องได้โดยใช้ Zipkin ของแท้ ดังนี้

  1. Start Zipkin ด้วยคำสั่ง sudo docker run -d -p 9411:9411 openzipkin/zipkin
  2. เช็คว่าสามารถเข้า Zipkin ได้ที่ http://localhost:9411 (ใครใช้ Docker for Mac/Windows อาจจะต้องเข้า IP ของ VM แทน)
  3. แก้ไข ZIPKIN_SERVERS ใน settings.py ให้ชี้ไปที่ http://localhost:9411 แทน
  4. ลองเข้าเว็บ Django ของเราแล้วรีเฟรชสองสามที
  5. ใน Zipkin กด Find Traces น่าจะปรากฏ Trace ขึ้นมา

(Note: py-zipkin จะสะสม trace ให้ครบ 100 อันก่อนส่ง ต้องเปิด DEBUG=true ถึงจะส่ง trace ทันที)

Trace ที่ได้จะหน้าตาประมาณนี้

สำหรับถ้าจะใช้บน Google Cloud Trace จะหน้าตาแบบนี้

เปรียบเทียบกับ X-Ray

จากที่ใช้งานมาทั้ง 2 platform แล้ว ผมพบว่าสิ่งที่ X-Ray จะนำเสนอคือความสัมพันธ์ระหว่าง service และ error ระหว่างทำงานมากกว่า เพราะมุมมองการดู Trace นั้นสามารถใช้งานได้สะดวกมาก และมีแผนภาพระหว่าง service ด้วย

สำหรับทางฝั่ง Stackdriver Trace นั้น จุดขายคือ Auto analysis ครับ

พอเราใช้ไปสักสัปดาห์นึงแล้ว URL ไหนที่ hit บ่อยที่สุด top 3 จะถูกนำมาเทียบกับสัปดาห์ที่แล้วโดยอัตโนมัติ เพื่อให้เราเห็นว่าที่เราทำไปในสัปดาห์นี้นั้นทำให้เว็บเร็วขึ้นหรือช้าลงหรือเปล่า และสามารถกดดูตัวอย่าง Trace ใน percentile ต่างๆ ได้ด้วย (ถ้าใช้ X-Ray จะต้องไปลากในกราฟเอาเอง)

โดยส่วนตัวผมชอบแบบ X-Ray มากกว่า เพราะอยากรู้เรื่อง error เป็นหลักมากกว่าซึ่งใน Trace มันดูไม่ได้ (เข้าใจว่าน่าจะให้ไปใช้ Stackdriver Error Reporting แทน แต่ผมใช้ Sentry อยู่แล้ว)

สุดท้าย Stackdriver Trace ฟรี 100% ครับ ไม่มีข้อจำกัดใดๆ ทั้งสิ้น ฉะนั้นที่ TipMe ตอนนี้ก็จะยิงทุก request เข้าหมดทุกอันได้เลย