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

Kubernetes Audit Log 使用筆記

我在公司的工作環境中,有些業務需要部屬服務在 Kubernetes (下稱 K8s) 上。 因此在專案早期,部門內的同事自架了 K8s cluster 來開發。

隨著時間流逝,各個 RD 開始上手 K8s 操作後,每天都有人在對 K8s 的 master 開發環境做修改。 於是部門內開始產生一些令人煩躁的對話

  • 我看 K8s 上面有裝了某個 CRD,但沒有裝對應的 service 來用這個 CRD,這個是你裝的嗎?
  • Test namespace 裝了一個 Ingress rule 產生衝突了,那個 rule 是誰裝的?

這些對話的共通點是:想知道 K8s 的狀態改變是誰造成的。 但在部門自架的環境內,因為大家共用了一個 kubeconfig,所以根本無從找起..

於是我想辦法把開發用的 K8s 環境設定好 auditing log 的功能,並留下這篇筆記

Audit 目標

要做 audit 來確認每個人做了什麼操作,我需要達到兩個目標

  1. 不同人員需要使用不同的身分存取 K8s API server
  2. API server 需開啟 log 且 log 需保存在 persistent storage 上

身分驗證方式比較

參考 K8s 的 Authenticating 官方文件,在不依賴外部服務的情況下,大概有三種身分驗證的方式

  • X509 Client Certificate
  • Static Token File
  • Service Account Tokens

以下分別介紹各方式的優缺點

X509 Client Certificate

此方式依賴 TLS 的 client verification 功能,只要你有正確的憑證塞在 kubeconfig 裡即可使用。 一般在做 cluster 初始化過程中拿到的 admin kubeconfig ,其內容即屬這一類。

此方式的優點為

  • 若採取嚴謹的使用者自行產生 key-CSR pair 再給 CA 簽署流程,因為僅使用者有 private key,出事時有高度信心一定是該使用者所為
  • 除了自己的 user name 外,使用者可以從自己的憑證中直接確認操作 K8s 時會有那些 group 身分
    • 憑證內 subject 的 CN 對應 K8s user name, O 對應 K8s group name

此方式的缺點為

  • K8s 不支援 X509 原生的 certificate revocation 功能,若有特定 client 憑證有問題,得整個 CA 換掉重來

Static Token File

K8s API server 在開啟時,可以設定一個檔案來記錄 token 與 user(group) 的 mapping 關係。 Client 連上 API server 時,只要能拿出此 token,便會被視為對應的 user 進行後續權限檢查。

此方式的優點為

  • 設定簡單。需要新增/刪除使用者或修改 token 時,只需修改一個檔案
  • Token 可長可短,可以做出較為可讀的 kubeconfig 檔案 (行寬 80 字元以內)

此方式的缺點為

  • static token file 設定有異動時需要重開 server

Service Account

Service Account 是 K8s 原生設計給 K8s 內的 service 做 K8s 自我管理的機制。

此方式的優點為

  • 彈性極高,可在 runtime 直接透過 K8s API 產生新的 service account

此方式的缺點為

  • service account 屬 namespaced resource,若有多個 namespace 要相同 user,需要重複設定
  • 產生的 audit log 較難做事後梳理
    • K8s 有大量利用 service account 的自我管理行為,因此難以區隔使用者操作和 K8s 自身操作
  • 相較於 X509 或 static token 方式,service account 不能直接設定群組

環境說明

若使用 kubeadm 安裝設定 K8s cluster,只有 kubelet 會作為一個 system service 運行在 host 中。 其他如 K8s API server, scheduler 及 etcd 等都是跑在 master node 的 Docker container 環境中

以下說明均假設為此類環境進行操作。

設定 Static Token File

K8s Master Node 設定修改

新增 user token file /etc/kubernetes/tokens.csv (路徑可自行調整)

1
2
user-token,user-name,uid,"optional-group,another-group"
fc27911e-73dd-46b0-8c57-86f2fe5fdd21,alice,alice@example.com,"developer"

檔案為單純的 CSV 格式,包含四個欄位

  • User Token: 任意字串,不一定要使用 UUID 格式
  • User Name: 使用此 token 身分驗證完成後得到的 user name
  • UID: 用途不明,會出現在 audit log 中
    • identifies the end user and attempts to be more consistent and unique than username

  • List of Group Name: (Optional) 使用此 token 身分驗證完成後得到的 group 身分

設好 static token file 後,修改 API server 的 static pod 描述 /etc/kubernetes/manifests/kube-apiserver.yamluser-tokens 的 path 與前述設定對齊。

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
diff --git a/root/manifests/kube-apiserver.yaml b/root/token-api-server.yaml
index 31c5f40..d4511ae 100644
--- a/root/manifests/kube-apiserver.yaml
+++ b/root/token-api-server.yaml
@@ -37,6 +37,7 @@ spec:
- --service-cluster-ip-range=10.96.0.0/12
- --tls-cert-file=/etc/kubernetes/pki/apiserver.crt
- --tls-private-key-file=/etc/kubernetes/pki/apiserver.key
+ - --token-auth-file=/etc/kubernetes/tokens.csv
image: k8s.gcr.io/kube-apiserver:v1.17.4
imagePullPolicy: IfNotPresent
livenessProbe:
@@ -71,6 +72,9 @@ spec:
- mountPath: /usr/share/ca-certificates
name: usr-share-ca-certificates
readOnly: true
+ - mountPath: /etc/kubernetes/tokens.csv
+ name: user-tokens
+ readOnly: true
hostNetwork: true
priorityClassName: system-cluster-critical
volumes:
@@ -98,4 +102,8 @@ spec:
path: /usr/share/ca-certificates
type: DirectoryOrCreate
name: usr-share-ca-certificates
+ - hostPath:
+ path: /etc/kubernetes/tokens.csv
+ type: FileOrCreate
+ name: user-tokens
status: {}

上述修改內容的重點為

  • 將 master node 上的 user token 設定檔 mount 至 API server 的 container 內
  • 設定 API server 去使用此 token 檔案

User Token File 後續維護

若之後需要修改 user token file,因為一些上游的限制, API server pod 無法觀測到檔案的修改,即使 kill pod 再重啟也無法使用新的 token file。

不過我們可以透過修改 API server 描述檔的方式,穩定地重新部屬 API server,讓新的 token file 生效。

  • 編輯 /etc/kubernetes/tokens.csv
  • 修改 API server 描述檔 /etc/kubernetes/manifests/kube-apiserver.yaml
    • 加入或修改 metadata.annotations.lastModify 欄位,填入合適字串
  • 修改後 kubelet 會偵測到檔案異動,並重新 apply apiserver pod

User kubeconfig 設定

使用 kubectl 設定 user token

kubectl config set-credentials <user-name> --token=<token>

或是直接修改 kubeconfig 內的 user object

1
2
3
- name: alice
user:
token: fc27911e-73dd-46b0-8c57-86f2fe5fdd21

Log 設定

當各個使用者操作 K8s 的身分確實有被切分開之後,即可進行後續的 audit log 設定動作。

Audit log 必須在吻合事先設定的 match rule 才會被記錄下來。 根據 Auditing 文件 說明, server 在判斷每個事件的 log level 時,是採取 first match 的規則進行。第一個吻合的規則會決定此事件是否紀錄以及紀錄的詳細程度。

The first matching rule sets the “audit level” of the event.

API Server Audit 設定

在 master node 上設定 audit policy /etc/kubernetes/audit-policy.yaml (路徑可自行調整)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
apiVersion: audit.k8s.io/v1
kind: Policy
omitStages:
- "RequestReceived"
rules:
- level: Metadata
userGroups:
- "developer"
verbs: ["create", "update", "patch", "delete", "deletecollection"]
- level: Metadata
userGroups:
- "developer"
resources:
- group: ""
resources: ["secrets", "configmaps"]

此設定有幾個重點

  • global 的 omitStages 設定
    • 所有 API request 都會經過 RequestReceived stage
    • 省略此 stage 可以避免所有的 request 都產生兩筆 log
  • Rule 以 userGroups 進行篩選
    • 若已知要紀錄的 user group 範圍,明定 group 可避免記錄到大量的 K8s 自身維護的事件
  • 設定動詞範圍記錄所有的 modify 操作
  • 設定敏感的 resource 種類 (e.g. secrets & configmaps) 記錄所有操作

接著修改 API server 的 static pod 描述 /etc/kubernetes/manifests/kube-apiserver.yamlaudit hostPath volume 需與前述設定對齊

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
diff --git a/root/token-api-server.yaml b/root/audit-token-api-server.yaml
index d4511ae..0e07f7f 100644
--- a/root/token-api-server.yaml
+++ b/root/audit-token-api-server.yaml
@@ -38,6 +38,10 @@ spec:
- --tls-cert-file=/etc/kubernetes/pki/apiserver.crt
- --tls-private-key-file=/etc/kubernetes/pki/apiserver.key
- --token-auth-file=/etc/kubernetes/tokens.csv
+ - --audit-policy-file=/etc/kubernetes/audit-policy.yaml
+ - --audit-log-path=/var/log/kubernetes/audit.log
+ - --audit-log-maxsize=1
+ - --audit-log-maxbackup=6
image: k8s.gcr.io/kube-apiserver:v1.17.4
imagePullPolicy: IfNotPresent
livenessProbe:
@@ -75,6 +79,12 @@ spec:
- mountPath: /etc/kubernetes/tokens.csv
name: user-tokens
readOnly: true
+ - mountPath: /etc/kubernetes/audit-policy.yaml
+ name: audit
+ readOnly: true
+ - mountPath: /var/log/kubernetes
+ name: audit-log
+ readOnly: false
hostNetwork: true
priorityClassName: system-cluster-critical
volumes:
@@ -106,4 +116,12 @@ spec:
path: /etc/kubernetes/tokens.csv
type: FileOrCreate
name: user-tokens
+ - name: audit
+ hostPath:
+ path: /etc/kubernetes/audit-policy.yaml
+ type: File
+ - name: audit-log
+ hostPath:
+ path: /var/log/kubernetes
+ type: DirectoryOrCreate
status: {}

Note: 開 /var/log/kubernetes 資料夾而非單一 log 檔案,是為了避免 log rotate 時因權限不足無法正確 rotate

設定完之後即可在 master node 的 /var/log/kubernetes 看到 access log

Sample 如下

command: kubectl apply -f services/tasks/redis-cluster-proxy.yml

log: (Log 檔內會寫成一行,beautify 後如下)

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
{
"kind":"Event",
"apiVersion":"audit.k8s.io/v1",
"level":"Metadata",
"auditID":"f09f32f4-a93f-41ee-b2b9-2f3acf3aa963",
"stage":"ResponseComplete",
"requestURI":"/api/v1/namespaces/alice/services",
"verb":"create",
"user":{
"username":"alice",
"uid":"alice@example.com",
"groups":[
"developer",
"system:authenticated"
]
},
"sourceIPs":[
"10.300.400.512"
],
"userAgent":"kubectl/v1.18.2 (linux/amd64) kubernetes/52c56ce",
"objectRef":{
"resource":"services",
"namespace":"alice",
"name":"redis-cluster-proxy",
"apiVersion":"v1"
},
"responseStatus":{
"metadata":{},
"code":201
},
"requestReceivedTimestamp":"2020-10-21T12:27:30.252440Z",
"stageTimestamp":"2020-10-21T12:27:30.272401Z",
"annotations":{
"authorization.k8s.io/decision":"allow",
"authorization.k8s.io/reason":"RBAC: allowed by RoleBinding \"super-user-role-binding-alice/alice\" of Role \"super-user\" to User \"alice\""
}
}

疑難排解

設定檔位置

kubelet Static Pod 設定資料夾不一定在 /etc/kubernetes/manifests 位置, 須從 kubelet 啟動設定中的 staticPodPath 欄位找到真實位置。

備份設定檔

若要備份 static pod 設定資料夾內的任何檔案,不能備份在相同資料夾內,否則會導致 kubelet 行為怪異。

Reload K8s API server 設定

kubelet service 一般會自動偵測 static pod 資料夾內的檔案異動,並重新佈署該 pod,但偶爾還是會碰上意外..

發生意外時,以下方式可能可以回到正常狀態

  • 刪除對應的 pod, e.g. kubectl delete -n kube-system pod kube-apiserver-<cluster name>
    • 刪除後 kubelet 會馬上重新佈署一個新的 API server
    • Controlled By: Node/k8s-master: 意味者此 pod 不是由 deployment 等 K8s object 控制,是直接由 master node 控制
  • 或是重啟 kubelet systemd service

後續

此篇筆記紀錄 static token 的身分驗證機制,但若有企業規模的身分驗證需求時,這顯然不是個好方法。

Kubernetes 也有原生支援 OpenID 的身分驗證方式來應付更進一步的需求,不過這部分就等未來有空再來研究了。

References

Appendix

Request Stages:

1
2
3
4
5
6
7
8
9
10
11
12
 +-----------------+
| RequestReceived +----+
+---+-------------+ |
| |
| +----------v------+
| | ResponseStarted |
| +----------+------+
| | +-------+
| | | Panic |
+----v------------+ | +-------+
| ResponseComplete<-----+
+-----------------+