如何避免 Commit Message 拼錯字?

文件打錯字還好,隨時可以修。 但在 commit message 中打錯字,可是會流傳千古。

身為一個 RD,有個極簡的解決方法.. 把底下這行設定放到 .vimrc 內即可。 (O

1
autocmd FileType gitcommit setlocal spell

(對於屬 Git commit message 的 buffer 自動啟用 spell check 功能)

設定完之後,當出現 vim 不認識的單字時,就會有醒目的顏色提示, 提醒自己該回頭看一下是不是又拼錯字了。

當然,要有舒適的拼字檢查體驗,字典檔的維護也是很重要的一環。 不過那又是另一個話題了..

Reference

Google 更新 Go 的社群行為準則

昨天 Go blog 上出新文章,說要更新 Code of Conduct。

一直一來覺得每個社群的 CoC 都寫得差不多,不外乎是要互相尊重、開放透明、建設性發言等等。 也因為都差不多,平常也不會去細看。反正就是些正常人該有的道德觀。 因此,看到說要更新 Code of Conduct 讓我感到有點好奇。

讀一讀讀下去,其實這次 Go community 的 CoC 就是新增一條:

Be responsible. What you say and do matters. Take responsibility …

沒想到連這個都要寫進 CoC …。可能 Go 的核心開發團隊看 issue 真的看到心累了? XD

See: Code of Conduct Updates - go.dev

單台機器的 Ceph 部署

原由

Ceph 的預設設定對資料的 replica 行為要求嚴格。 若只有單台機器或是硬碟數量受限,往往架設起來的 Ceph 無法順利存放資料。

此篇筆記關注的目標如下

若想要用最少資源,建立可用的 Ceph 環境,需要做哪些額外的調整?

背景知識

Ceph 是一套開源的儲存叢集 solution。 可以整合多個儲存設備並在其上提供 RADOSGW, RBD, Ceph FS 等不同層級的存取介面。

ceph-stack

對於每個儲存設備 (HDD, SSD),Ceph 會建立對應的 OSD 來管理儲存設備。 有了儲存設備之後,Ceph 會建立邏輯上的 pool 作為管理空間的單位。 Pool 底下會有多個 PG(placement group) 作為實際存放資料至 OSD 中的區塊。

在 Ceph 的預設設定中,一般 pool 的 replica 行為如下

  • 要有三份 replica
  • replica 要分散在不同的 host 上

在開發環境中,資料掉了其實並無傷大雅,三份 replica 意味著儲存空間的浪費。 且若資料真的要放在不同的 host 上,連同 replica 三份這點,我們就至少要開三台機器, 增加無謂的管理成本。

解決方式

假設我們都是透過 cephadm bootstrap 來架設 Ceph。

Ceph cluster 建立好,也設定完需要的 OSD 之後,就可以來建立 pool。

根據 pool 的目的不同,要解決單台機器部署 Ceph 的限制,大概會有兩種做法。

降低 Pool Size

Pool size 此術語意味著該 pool 下的 PG 要 replica 幾份。 若某 pool 是拿供他人存放資料,或是會使用較多空間的,可以把 size 降為 1。

調整完之後就相當於該 pool 內的所有資料都不會有 replica。

範例如下:

1
2
3
ceph osd pool create "<pool name>"
ceph osd pool set "<pool name>" size 1
ceph osd pool application enable "<pool name>" rbd

調整 Choose Leaf 行為

Ceph 有定義不同層級的資料分散設定。 預設值為 host,意味著只有一台機器的情況下,資料會無法複製。 若調整為 osd,只要該機器上有多顆硬碟即可滿足複製條件。 若是針對 Ceph 自行建立出來,管理 meta data 的 pool (e.g. device_health_metrics) 可以考慮使用此方式處理。

設定方式大概有兩種。

方法一: 調整 Ceph global 設定

編輯 /etc/ceph.conf 並在 global section 下加入 osd_crush_chooseleaf_type 設定

1
2
3
[global]
...
osd_crush_chooseleaf_type = 0

或是直接執行 command

1
ceph config set global osd_crush_chooseleaf_type 0

這邊的 0 代表 OSD。預設的對應列表如下

1
2
3
4
5
6
type 0 osd
type 1 host
...
type 9 zone
type 10 region
type 11 root

方法二: 修改 crush map 內容

筆者有注意到有時即使有執行方法一,pool 還是不會受到設定影響。 (相關知識還太少, 不太確定具體原因) 不過針對此狀況,還有第二個方法可以使用。

此方法會用到 crushtool 指令 (Ubuntu 中需要額外安裝 ceph-base 套件)

首先執行指令將目前的 crush map 撈出來

1
2
ceph osd getcrushmap -o "compiled-crush-map"
crushtool -d "compiled-crush-map" -o "crush-map"

接著修改 crush-map 檔案內容,應該會有一行有 step chooseleaf 開頭的設定,把最後的 type 從 host 調整為 osd

1
2
3
4
# Before
step chooseleaf firstn <number> type host
# After
step chooseleaf firstn <number> type osd

最後將修改好的 crush map 設定塞回去。

1
2
crushtool -c "crush-map" -o "compiled-crush-map"
ceph osd setcrushmap -i "compiled-crush-map"

相關 reference link

結語

筆者在公司業務並不負責維護 production 的 Ceph cluster,僅是為了建立 Kubernetes 開發環境,需要有個基本會動的 Ceph。

為了用最少資源建立 Ceph 環境,需要調整相關設定來改變 Ceph 行為。 只可惜相關的資源不是很夠,一路跌跌撞撞下來,決定寫下這篇筆記,希望造福未來的自己,也同時照顧他人。

NATS 與 JetStream 簡易介紹

最近因公司業務在玩一套相對新的 MQ: NATS。 因為官方文件不慎清楚且有些地方與直覺不同,造成起步緩慢。

以下簡單紀錄一下剛入門時應知道的事情。

Guide

相較 RabbitMQ, Kafka 等,NATS 是一套較為年輕的 MQ。 雖然有部分子專案的版本未達 v1.0,但官方宣稱已經接近 production ready。

NATS 從一開始就是針對 cloud service 設計,cluster mode 的水平擴展, node 之間的身分驗證及 TLS 通訊設計看起來都還不錯。

NATS 的 message 並無特別限制,在 client library 內任何的 byte sequence 都可以成為 message。

NATS 有以下三個模式(以及其對應的 client library)。

NATS (NATS Core)

NATS 專案從一開始發展時的基本模式。 支援 Pub/Sub pattern 並提供 at-most-once 語意。

NATS Streaming

NATS Streaming 是一套疊在 NATS 上面形成的 solution。

因為設計上的問題,後來又有了 JetStream,所以我們基本上不用理它,只要知道 NATS Streaming 和 JetStream 不一樣,翻文件的時候不要翻錯即可。

JetStream

JetStream 是後來做在 NATS 內,可選擇是否啟用的子系統。 藉由 JetStream, 可以實作 Producer/Consumer pattern 並提供 at-least-once 語意。

Server side 沒什麼需要注意的,只要用較新版的 NATS image 並啟用設定即可。 Client 開發則需要注意一些概念。

  • Subject: NATS 最初的概念,代表一些 message 的集合。
  • Stream: 建立於一或多個 Subject 之上,可將這些 subject 內的 message 統整起來,並放入 persistent storage。
  • Consumer: 建立在某個 Stream 之下,可以依序的 consume 屬於此 stream 的特定 message。

需要注意的是,不只 Subject 與 Stream,Consumer 本身也是建立在 NATS server 中的一個物件。 當利用 client library create 一個 Consumer 時,並不是該 process 本身成為一個 consumer, 而是 NATS server 中被創了一個 Consumer 物件,準備去使用 Stream 裡面的 message。

JetStream client library 並沒有提供一個對稱的 producer/consumer API。 基於術語的限制以及為了避免誤會,以下在稱呼一般所稱的 producer/consumer 時, 會特別加上 role 後綴來表示。

Producer role: 要使用 NATS library 內的 Publish API,將產生的 message 推送至 某個 Subject 內。

Consumer role: 要使用 JetStream library 內的 Stream API,在 NATS server 上對目標 Subject 建立 Stream,接著使用 JetStream Consumer API,在 NATS server 中 建立屬於該 Stream 的 Consumer。以上都完成之後,即可利用 Consumer 上的 NextMsg 來 消耗 message。

Conclusion

JetStream 的 API 設計並不常見,需要先認知到與既有設計的差別之處才能開始開發。 不過其 cloud native 的架構設計或許可以在維運上面勝過其他老牌的 MQ solution。

今天就先寫到這裡,如果有哪天有興趣再補吧。 :D

Reference

Appendix

Golang sample code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
package main

import (
"context"
"fmt"
"log"
"math/rand"
"os"
"testing"
"time"

"github.com/nats-io/jsm.go"
"github.com/nats-io/jsm.go/api"
"github.com/nats-io/nats.go"
)

var fullSubject string = "report_task.scheduled"
var wildcardSubject string = "report_task.*"

func consumeOne(doneChan chan bool) {
msg, err := consumer.NextMsg()
if err != nil {
fmt.Printf("Fail to get message: %v\n", err)
}
fmt.Printf("Consume get task: %s\n", string(msg.Data))
time.Sleep(2 * time.Second)
if err := msg.Ack(); err != nil {
fmt.Printf("Fail to ack the message %s: %v\n", string(msg.Data), err)
}
doneChan <- true
}

func TestProduceAndConsume(t *testing.T) {
producerStopChan := make(chan bool)
consumerStopChan := make(chan bool)
var taskCount int = 1

go func() {
for idx := 0; idx < taskCount; idx++ {
taskName := fmt.Sprintf("task #%d", rand.Int())
nc.Publish(fullSubject, []byte(taskName))

fmt.Printf("Producer produce: %s\n", taskName)
}
producerStopChan <- true
}()

for idx := 0; idx < taskCount; idx++ {
go func() {
consumeOne(consumerStopChan)
}()
}

<-producerStopChan
for idx := 0; idx < taskCount; idx++ {
<-consumerStopChan
}

fmt.Println("Done")
}

var ctx context.Context
var cancel context.CancelFunc
var nc *nats.Conn
var stream *jsm.Stream
var consumer *jsm.Consumer

func setup() {
var err error

ctx, cancel = context.WithTimeout(context.Background(), 10*time.Second)
nc, err = nats.Connect(nats.DefaultURL, nats.UserInfo("name", "password"), nats.UseOldRequestStyle())
if err != nil {
log.Fatal(err)
}

jsmgr, err := jsm.New(nc)
if err != nil {
log.Fatal(err)
}

streamName := "ReportTask"
stream, err = jsmgr.LoadOrNewStreamFromDefault(streamName,
api.StreamConfig{
Subjects: []string{wildcardSubject},
Storage: api.FileStorage,
Retention: api.LimitsPolicy,
Discard: api.DiscardOld,
MaxConsumers: -1,
MaxMsgs: -1,
MaxBytes: -1,
MaxAge: 24 * time.Hour,
MaxMsgSize: -1,
Replicas: 1,
NoAck: false,
})
if err != nil {
log.Fatal(err)
}

consumerName := "Generator"
consumer, err = stream.LoadOrNewConsumerFromDefault(consumerName,
api.ConsumerConfig{
Durable: consumerName,
DeliverPolicy: api.DeliverNew,
FilterSubject: fullSubject,
AckPolicy: api.AckExplicit,
AckWait: 30 * time.Second,
MaxDeliver: 5,
ReplayPolicy: api.ReplayInstant,
SampleFrequency: "0%",
})
if err != nil {
log.Fatal(err)
}

}

func shutdown() {
cancel()
}

func TestMain(m *testing.M) {
setup()
code := m.Run()
shutdown()
os.Exit(code)
}

GitLab 更換自家 GPG Key

今天 GitLab 在自家 blog 上公告 revoke 簽署 package 的 GPG key。

We recently became aware of an instance where this key and other tokens used to distribute official GitLab Runner packages and binaries were not secured according to GitLab’s security policies.

We have not found any evidence of unauthorized modification of the packages or access to the services storing them.

並不是因為 key 被 compromise,僅是因為 key 不符合公司的安全規範,所以就進行了一次 rekey。

GPG key rekey 並不如換憑證一樣,只要重簽一張就好 (因為信賴建立在已知的第三方 CA 上)。 GPG key rekey 需要透過可信管道重新宣告 fingerprint 並請大家 import 新的 key。 這個轉換的成本,相較換憑證應是高非常多且難以量化的。

沒想到居然僅為了不合安全規範就進行 rekey,不愧是國際一線的軟體公司!

See: The GPG key used to sign GitLab Runner packages has been rotated | GitLab

VS Code 新功能: Remote Repositories

VS Code 在 1.57 版中, Remote Development 系列 extension 加入了新成員: Remote Repositories

有了這個 extension 之後,如果遇上臨時想看的 project,就可以直接在 VS Code 中叫出來看,不需要事先 clone 至某個 local 資料夾。

不過.. 因為這個 extension 實際上是建一個 Virtual Workspaces 並把 code 放在裡面閱覽, 所以用 Remote Repositories 開出來的 workspace 功能非常受限。 諸如 Debug, Terminal 及大部分的 extension 基本上都不能用。 但話雖如此,當看 code 看一看想要開始進行比較深入的修改及除錯時, 其實也是有提供轉換成一般 workspace 的功能。 使用上非常方便!

可惜的是,目前此 extension 支援的 remote repository 種類只有 GitHub。 且如同其他 Remote Development Series,這個 extension 並非 open source project:

未來會不會支援 GitHub 以外的 Git repositories,甚至其他種類的 VCS, 只能看微軟爸爸的眼色了。

GitHub Pages 與 GitLab Pages 架設 Blog

筆者最近把個人 blog 的產生工具從 GitHub Pages 預設的 Jekyll 換成 Hexo,有了一點心得。 而且不只 GitHub Pages, 筆者在公司業務中也有大量使用 GitLab Pages 來產生文件及測試報表,算是有累積不少經驗。

趁著印象還深刻時,寫點筆記,替這兩個相同性質的服務做基本的介紹。

Pages 服務與 Static Site Generator

GitHub / GitLab Pages 可以將一組靜態網頁內容 (html, css, js 等),透過 GitHub / GitLab 的伺服器,host 在某個 URL 底下。 網頁產生工具 (Static Site Generator, 下稱 SSG) 則是 一個可以將用 Markdown 撰寫的文章,轉化成漂亮的靜態網頁內容的工具。常見的 SSG 有 Jekyll(Ruby), Hugo(Go), Hexo(JavaScript) 等。

若將 SSG 工具與 GitHub / GitLab Pages 服務,搭配使用, 寫作者只需要寫寫簡單的 Markdown 並 push commit,就能得到一個漂亮的 blog 或是文件網頁。 筆者的個人 blog 及公司的工作筆記即是使用這類流程架設。

整體流程大概如下圖所示:

1
2
3
4
5
6
7
8
9
               +   GitHub            +     github.io
Local Project | Project | site
| GitLab | gitlab.io
+ +

+----------+ +----------+ Build & +------+ User
| Markup | Push | Markup | Deploy | Site | Browse
| config.. | +----> | Config.. | +-------> | | +------->
+----------+ +----------+ +------+

GitHub Pages

GitHub Pages 基本上會有兩種主要的使用方式。 可以直接使用 GitHub Pages,或是透過 GitHub Pages 的 Jekyll 整合功能。 前者需要的技術背景與設定步驟均較複雜,後者較簡單但缺少了根據個別需求調整的機會。

Native GitHub Pages

若直接使用 GitHub Pages,使用方式是: 將 SSG 產生的網頁擺放至某 branch (預設為 gh-pages) 的 //docs 目錄。 每次該 branch 被更新時,GitHub 就會將最新版本的網頁內容, 呈現在 https://<username>.github.io/<project> 連結下。

早期這個 push brach 的動作是蠻麻煩的,但後來有了 GitHub Action 之後, 產生網站和後 push branch 的動作都可以在 GitHub 提供的環境完成,非常方便。

筆者個人使用的 job 描述檔如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# .github/workflows/blog.yaml

name: build-and-deploy-blog

on:
push:
branches: [ "master" ]
pull_request:

jobs:
blog:
runs-on: ubuntu-20.04
steps:
- name: Checkout
uses: actions/checkout@v2.3.4
- name: Setup Node.js environment
uses: actions/setup-node@v2.1.5
with:
node-version: 12.x
- name: Install dependent packages
run: npm install
- name: Build blog posts
run: npm run build
- name: Deploy
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./public

若不想使用 GitHub 提供的 domain,也可以參照 官方文件, 使用自己購買的 domain 來架設網站。 只要設定完成,GitHub 也可以一併幫使用者申請 custom domain 需要的 HTTPS 憑證。

比方說筆者的 blog 原本可存取的位置應是 https://wdhongtw.github.io/blog,但有設定 custom domain 後,目前是透過 https://blog.bitisle.net 來存取架在 GitHub Pages 上的 blog。

GitHub Pages Jekyll

前述 (Native) GitHub Pages 的使用方式會需要自己 push branch。 但若 GitHub 偵測到 project 使用的 SSG 是 Jekyll,GitHub 會自動處理產生網頁以及 後續部屬到 https://<username>.github.io/<project> 的工作。 (連 gh-pages branch 都省了,整個 project 會非常乾淨。)

此方法相當容易上手,也是 GitHub Pages 教學文件預設的使用方式。但因為產生網站的 環境是由 GitHub 協助處理,能使用的 Jekyll plugin 自然也是受到限制。

說是受到限制,但以一般使用情境應該也很夠了。諸如 RSS feed, sitemap, Open Graph metadata 等現代 blog 必備的功能都有自帶。 若想知道有那些 Jekyll plugin 可用,可至 Dependency versions | GitHub Pages 查閱。

Compare Native GitHub Page with GitHub Pages and Jekyll

簡單比較上述兩種方式如下

GitHub Page GitHub Page with Jekyll
SSG Tool on your choice only Jekyll
Deployment push generated site done by GitHub
Customization any plugins of SSG limited Jekyll plugin list

GitLab Pages

GitLab Pages 與 GitHub Pages 一樣,有將 SSG 產生的網頁 host 在特定網址的能力。 以 GitLab 官方 host 的站台來說,網站會放在 https://<username>.gitlab.io/<project> 下。 (私人或公司 host 的 GitLab instance 就要看各自的設定)

與 GitHub 不同的是,GitLab Pages 並不是透過 push branch 的方式部屬, 且沒有針對特定 SSG 提供更進一步的自動部屬功能。

GitLab Pages 的使用方式是基於 GitLab CI Pipeline 的架構設計的。若想要部屬 網站,一般的使用方式是在 pipeline 內產生網頁,接著將網頁內容擺放至為特定 job (pages) 的特定 artifacts 目錄 (public) 中。 一旦有 pipeline jobs 達成此條件。 GitLab 就會 把網頁內容部屬到對應的網址下。

筆者個人使用的 job 描述檔如下: (因為 GitHub Action 與 GitLab CI 的架構差異,寫起來比較簡潔)

1
2
3
4
5
6
7
8
9
10
11
12
13
# .gitlab-ci.yml
pages:
image: ruby:3.0.1
stage: deploy
before_script:
- bundle install
script:
- bundle exec jekyll build --destination public --baseurl "$CI_PAGES_URL"
artifacts:
paths:
- public
only:
- master

至於其他 GitHub Pages 有的功能,如 custom domain,自動申請 HTTPS 憑證等,GitLab Pages 也都有。 記得去 project 設定頁面設定即可。

Conclusion

在 2008 年 GitHub Pages 剛推出時,使用者都要自己手動 push gh-pages branch。 後來 GitLab 推出基於 GitLab CI 的 Pages 服務之後,GitHub Pages 使用體驗相較之下可說是非常糟糕。

但後來隨著 GitHub Actions 服務推出,以及社群維護的高品質 Pages 部屬 Action 出現。 GitHub / GitLab Pages 的使用體驗已經變得相當接近。 其他像是 custom domain 以及 HTTPS 的支援也都是免費的基本功能。

基於上述原因,許多早期的 比較文章 其實已經沒什麼參考價值。 若現在想架設新的 blog 等站台,只要選擇自己習慣的平台即可。

References

GitHub 即日起支援使用 Security Key 進行 Git 操作

GitHub 開始支援使用 security key 進行 Git 操作啦!

這應該是各家科技巨頭當中,第一個支援 security key 進行 SSH login 的服務吧。 筆者昨天 5/10 才在公司內分享如何使用 security key 來做 SSH login, 沒想到 Yubico 和 GitHub 也剛好在昨天一起同步更新 blog 文章,通知大家這個新功能。 喜極而泣..

以下簡單介紹如何使用這個新功能。 為了方便解說及避免誤會,後述內容均以正式名稱 authenticator 代稱 security key。

如何使用

首先當然要有一把 authenticator,如果還沒有,趕快去買一把囉。 :D

第一步,在 authenticator 內產生新的 key pair。

產生 key pair 的流程和傳統放在檔案系統上的差不多,只是 key type 要指定代表 authenticator 的 type。

1
ssh-keygen -t ecdsa-sk

產生的過程,根據不同的 authenticator,會有要求按一下 且/或 輸入 PIN code 驗證身分。 此步驟會在 authenticator 內產生一組 key pair,並將 public key 寫到檔案系統上。 平常放 private key 的那個檔案還是會產生,不過這次裡面放的會是一個 key handle。

第二步,透過 GitHub 的 web UI 上傳 public key

上傳完之後,沒意外就可以順利使用了,可以試著 clone 一些 project

1
2
3
4
$ git clone git@github.com:github/secure_headers.git
Cloning into 'secure_headers'...
Confirm user presence for key ECDSA-SK SHA256:........
User presence confirmed

若確認 OK,之後的 Git 操作都可以透過隨身攜帶的 authenticator 保護, 只要按一下 authenticator 即可。

設定完之後,若沒有拿出 authenticator,就不能進行 push / pull 操作。 只要確保 authenticator 還在身上,就可以安心睡大覺。 (再也不用擔心 private key 放在公司電腦上會被摸走了!)

進階使用方式

FIDO 2 authenticator 博大精深,除了上述的基本使用流程外,還有些細節可以設定。

以下分別介紹三個進階使用技巧:

要求身分驗證 (User Verification) 才能使用 Authenticator

根據每個人平常保管鑰匙習慣的差異,可能有人會擔心 authenticator 真的被摸走。

但 authenticator 也有支援一定要驗甚身分 (e.g. PIN code or 指紋辨識) 後才能 使用內部的 key pair 的功能。

要如何使用呢? 只要在產生 key pair 的過程中多下一個 flag 即可。

1
ssh-keygen -t ecdsa-sk -O verify-required

若之後要使用由此方式產生的 key pair,除了手指按一下 authenticator 之外,還會要求 使用者輸入 PIN code 才能順利完成操作。如此即使 authenticator 被偷走了,也不用太緊張。

全自動使用 Authenticator (避免 User Presence Check)

若把 authenticator 插上電腦後,想要隨時隨地都進行 push / pull, 但不要每次都手按一下 authenticator。這種自動化的使用情境 OpenSSH 其實也是有支援的。

使用的方式也是在產生 key pair 時多帶一個參數。

1
ssh-keygen -t ecdsa-sk -O no-touch-required

同時在將 public key 部屬到目標機器上時,在 public key 該行前面多下 no-touch-required 即可。 (詳情請見 ssh-keygen(1)sshd(8))

以此方始產生的 key pair 在使用時就不需要每次都手按一下,可以全自動使用。

不過,雖然 OpenSSH 有支援此種使用情境,但目前 GitHub 禁止這種使用方式

節錄自上述 blog 文章

While we understand the appeal of removing the need for the taps, we determined our current approach to require presence and intention is the best balance between usability and security.

所以在 GitHub 上,若想要全自動操作,只能回去用一般的 SSH key 或 API token 囉。

避免手動複製 Key Handle

前面有提到,原先檔案系統上用來放 private key 的檔案會變成拿來擺放 key handle。

這意味著,當我們想在新的機器上透過 SSH 進行 Git 操作時,除了拿出 authenticator 之外, 也需要把原先的 key handle 檔案複製到新的機器上。 且若 key handle 檔案掉了,該組 key pair 就不能使用了。

若要避免此問題,就要用上 authenticator 的另一個進階功能 discoverable credentials / resident keys 。

1
ssh-keygen -t ecdsa-sk -O resident

使用此類型的 key pair 時,會在 authenticator 上消耗一些儲存空間。但換來的好處是, 使用者可以在新的機器上,把 key handle 從 authenticator 內抽出來。

1
ssh-keygen -K # extract discoverable credentials

如此就不用手動複製擺放 key handle 的 private key 檔案了。

但要注意的是此類型 key pair 會消耗 authenticator 的空間。 每把 authenticator 可以放多少 key pair 要再自行查閱官方文件。

結語

以上介紹了基本使用情境及三種可能的進階使用方式。

筆者在 2014 年第一次注意到 FIDO (U2F) 標準,當時就在想像沒有密碼的世界。 如今,藉由 FIDO 2 security key 的普及,當初所想的美好願景似乎在慢慢地實現中..

希望未來能看到 security key 運用在更多場景上!

延長或縮短 GPG 金鑰的過期時間 (Expiration Time)

筆者在 2018 年的時候開了第一個真的有在長期使用的 GPG 金鑰。

因為年少輕狂不懂事,當時特別把 primary key 設定成永遠不會過期。 但演算法可能在未來被發現漏洞,電腦的運算能力也會越來越好, 一把不會過期的 GPG 金鑰是先天上不合理的存在。 考量到此問題,筆者後來又再修正了該金鑰的過期時間, 以及整理這篇筆記…

GPG Key 可以延展過期時間?

我想這應該是熟悉 X.509 憑證生態系的人最為驚訝的一件事情了。

我發現幾位公司主管並不知道這件事情,也促使我在經過一段時間後回來整理這篇文章。 事實上,GPG key 不只是可以延展過期時間,這也是一般推薦的最佳慣例。

People think that they don’t want their keys to expire, but you actually do. Why? Because you can always extend your expiration date, even after it has expired!

See: OpenPGP Best Practices - riseup.net

使用者應該設一個較短的有效時間,並在後續有需要時延展過期時間。

GPG key 可以自己修改金鑰的過期時間,是因為 GPG key 和 X.509 憑證有著本質上的區別。

GPG key 的產生是透過 primary key 的 self-signature, 而 X.509 憑證的簽署是由公正的第三方 CA 進行。

X.509 憑證的過期時間是 CA 幫你簽署憑證時決定,自然無法隨意修改, 大家也很習慣這件事情,但 GPG key 就不一樣了。 GPG key 的有效時間是透過 key 的 self-signature 內所記載的時間決定。 只要 primary (private) key 沒有遺失,持有者隨時可以重新自簽並修改時間。

只要認知到兩者本質上的差異,可以修改過期時間這件事情也就很好理解了。

他人如何認定過期時間?

既然 GPG key 可以隨時重簽修改過期時間,那對他人來說, 該如何判定某把 key 究竟什麼時候過期呢?

規則很簡單

The latest self-signature takes precedence

See: Key Management

若是透過 gpg tool 修改過期時間,舊的 self-signature 會被刪掉。 因為只有一個 self-signature,修改完之後,只要重新把 key export 給他人, 他人就可以知道新的過期時間。

若不是透過信賴管道直接把新簽的 key 給他人,而是透過 GPG key server, 狀況會有點不一樣。

基於安全考量,GPG key server 是不允許部分或完全刪除 key 的,MIT 名下的 key server 還特別寫了一篇 FAQ 來說明這件事。 對於一把已存在的 key,使用者只能推新的 sub key 或新的 signature 上去。

因此,他人透過 key server 取得 key 時,也會拿到多個 signature。 好在 signature 本身也有時戳,根據上述 “後者為準” 的規則,他人就可以知道 正確的過期時間是何時。

有興趣的可以查看筆者的 GPG key 來確認這個行為

1
2
3
4
5
6
7
8
gpg --keyserver keys.gnupg.net --recv-keys C9756E05
# Get key from key server

gpg --export C9756E05 | gpg --list-packets
# One signature has "key expires after ..." while another doesn't

gpg -k C9756E05
# Validate that the key indeed expires at some time

或是可以直接去 GnuPG 官方的 key server 查看: Search results for ‘0xc728b2bdc9756e05’

結語

翻閱文件研究的過程,慢慢感受到到 GPG 這個扣除 X.509 之外唯一成熟的 PKI 生態系,究竟有多麼偉大。同時也看到很多值得細讀的 guideline 文件。

若有時間,真的該來好好吸收整理。

保護存在檔案系統上的 Docker 登入密碼

在企業內部的工作環境中,常會碰到需要存取 private registry 上的 image 的狀況。 以 Docker 的工作流程來說,一般要透過執行 docker login 來存取 private registry。 不過,若事先毛設定好 docker credential helper,執行 docker login 會導致 我們的密碼 / API token 直接以明文的方式寫在檔案系統上。

這篇筆記說明如何在 Linux 環境下安裝與設定 docker credential helper

Installation

至 docker/docker-credential-helpers 的 GitHub release 頁面下載最新版本的 docker-credential-secretservice

1
curl -L https://github.com/docker/docker-credential-helpers/releases/download/v0.6.3/docker-credential-secretservice-v0.6.3-amd64.tar.gz >secretservice.tar.gz

解壓縮並把執行檔放到任意一個 PATH 資料夾內。

1
2
3
tar -zxv -f secretservice.tar.gz
chmod +x docker-credential-secretservice
mv docker-credential-secretservice ~/.local/bin

Configuration

為了讓 docker 工具知道我們要用 credential helper,需要調整家目錄下的設定檔。

在設定檔 ~/.docker/config.json 內加入 credsStore 設定。

1
$EDITOR ~/.docker/config.json
1
2
3
{
"credsStore": "secretservice"
}

註: 此資料夾和 JSON 檔案可能不存在。若沒有自己創一個即可。

註: 根據文件,此欄位的值與是 helper binary 的後綴對齊,因為 Linux 環境使用的 binary 是 docker-credential-secretservice 所以需要填入的值爲 secretservice

Usage

如果已經有登入過某 registry,需要手動登出。

1
docker logout registry.example.com

(重新) 登入該 registry。

1
docker login registry.example.com

檢視 ~/.docker/config.json 並確認對應的身分紀錄是空白的。

1
2
3
4
5
{
"auths": {
"registry.example.com": {}
}
}

若有安裝 Seahorse 程式的話,此時可以看到 secret 被放在 Login keyring 中。

如果設定錯誤的話,登入資訊會以編碼過的方式呈現在該紀錄中。

1
2
3
4
5
6
7
{
"auths": {
"registry.example.com": {
"auth": "c3R...zE2"
}
}
}

Future Reading