利用 SSH 建立 SOCKS Proxy

最近因為疫情又開始 WFH 了。 公司有提供一些 VPN solution 讓員工存取公司內網路, 但有一些架在 public cloud 上的服務後台因為有擋來源 IP,無法在家直接存取。

這時候 SSH 內建的 SOCKS proxy server 功能就可以派上用場了!

SSH 可以在建立連線時,一併在本機端開出一個 SOCKS (version 4 and 5) 的 server, 接下來任何應用程式都可以將任意的 TCP 連線透過這個 SOCKS server,轉送到 SSH server 後再與目標站台連線。 因為大家一定在公司裡有台可以 SSH 的機器(?),於是這種限制公司 IP 的管理後台就可以順利存取。 :D

使用方式很簡單,SSH 連線時多下參數即可。

1
ssh "target-machine" -D "localhost:1080" -N
  • -D localhost:1080: 決定要開在 local 的 SOCKS port,RFC 建議是 1080
  • -N: 如果不需要開一個 shell,只是要 SOCKS proxy 功能,那可以多帶此參數

Note: SSH 有支援 SOCKS5 (可做 IPv6 proxy) 但不支援 authentication,不過因為 SOCKS server 可以如上述設定只開在 localhost 上,所以沒麼問題。

接著我們就可以設定 OS 層級或是 application 層級的 proxy 設定來使用這個 proxy 了! 以我一開始遇到的問題來說,通常我會多開一個 Firefox 並設定使用 proxy 來存取公司的各種管理後台。 這樣就可以保持其他網路流量還是直接往外打,不需要過 proxy。 :D

若要快速啟動 proxy,可以使用 Windows Terminal 並設定一個 profile,執行上述 SSH 指令。

PuTTY 作為 Windows 上最多人使用的 SSH client,也有支援 SOCKS proxy 功能, 詳見: How To Set up a SOCKS Proxy Using Putty & SSH - Security Musings

Reference

Golang 1.18 Generics 終於來臨

今天 Golang 1.18 終於正式釋出啦! 我們終於有辦法在 Golang 裡做 generic programming 啦!

Golang 是一個近 10 年快速竄紅的程式語言,但在很多面向其實還是非常土炮。 得靠後續社群不斷的討論與貢獻才達到一個比較完善的水準。 像是..

  • context package in Golang 1.7: 解決 long job cancelling 的問題
  • errors package in Golang 1.13: 滿足其他語言常見的 error 嵌套需求和提供統一的判斷方式
  • Generic support in Golang 1.18: 提供開發者實作各種型無關演算法的機會

一直以來,在 Golang 的標準函式庫中,碰到類型無關的抽象問題時,最後給出來的解法大概就兩種

  1. 所有 input / output 參數都定義成 interface{},大家一起把型別檢查往 run-time 丟
  2. 同一個 class / function 對於常用的 data type 通通實作一遍

前者最典型的大概就是 sync 這個函示庫,後者.. 大家應該都看過那個慘不忍睹的 sort..

不過這些都是過去式了,從今天開始,大家都可以寫自己想要的 generic code / library / framework。 :D

Usage

基本語法很簡單,只要在想做成 generic 的 function / struct 後面多加一個 [T TypeName] 即可, TypeName 是用原本就有的 interface{...} 語法來表示,可以自己描述這個 generic function / struct 支援的型態必須滿足什麼樣的介面。

以 Python style 的 sort by key 當例子。 我們可以定義一個 generic 的 sort function,並且明定送進來的 list 內的每個元素需要支援一個 Key 函示, 作為排序時的根據。

範例 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
package main

type Keyable interface {
Key() int
}

func Sort[T Keyable](items []T) []T {
if len(items) <= 1 {
return items
}

pivot := items[0]
less, greater := []T{}, []T{}
for _, item := range items[1:] {
if item.Key() < pivot.Key() {
less = append(less, item)
} else {
greater = append(greater, item)
}
}

return append(append(less, pivot), greater...)
}

type Person struct {
Name string
Age int
}

func (n Person) Key() int {
return n.Age
}

func main() {
persons := []Person{{Name: "alice", Age: 33}, {Name: "bob", Age: 27}}

persons = Sort(persons)
}

在這個範例中,Sort 要求送進來的 []T 當中的 T 要實作 Keyable 介面 (提供 Key method)。 當我們想排序一堆 Person 時,我們可以在這個 Person 物件上定義 Key method,取出 Person 的年齡。 完成之後,我們就可以依年齡來排序 []Person 了。

期許自己未來可以多加利用這個遲來的功能.. XD

References

如何避免 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 運用在更多場景上!