หลายๆ คนน่าจะทราบดีว่าเราไม่ควร 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 นั้นๆ หรือเลี่ยงได้ก็จะไม่ใช้ ก๊อปไฟล์ระหว่างเครื่องเองอย่างเดียว