用oauth2-proxy和Github保护istio服务

在AWS EKS用上了istio后,部署服务很方便,但是我发现不少应用没有自带账号验证机制(比如jaeger),而很多数据信息比较敏感,那怎么办呢?Keycloak又太复杂了,这就想到了Github账号机制来管理,那要是能整合到istio里就太好了~说干就干

首先配置Github的应用App (官方文档),记好Client ID 和 Client secret,等回会用到。

配置好应用的oauth2 callback地址,比如 https://example.com/oauth2,等下istio需要配置对应的service。接着就是安装和配置oauth2-proxy(helm)

configuration:
  clientID: "xxxxx" #刚才的Github Client ID 
  clientSecret: "xxxxxxxxxxxxxxxxx" # 刚才的Github Client Secret
  ## 用这个命令生成一段随机的secret 
  ## openssl rand -base64 32 | head -c 32 | base64
  cookieSecret: "xxxxxxxxxxxxxxxxxxxxxxxx="
extraArgs:
  [
    "--provider=github", # provider 我们选github
    "--github-org=example", # 组织填入自己的组织名,还有其他验证范围可选,具体可以看文档
    "--scope=user:email", # 这个是oauth-proxy的bug……不加上会不停的重定向
    "--upstream=static://200", # 也是不加上就不停重定向的bug
    "--pass-authorization-header=true",
    "--pass-user-headers=true"
  ]

github-org这个配置可以改成你需要的验证方式,具体可以看oauth2-proxy官方文档(链接)。回到我们的istio配置上,给整个istio添加自定义的extensionProvider,让Github 的 AuthorizationPolicy能跑通。有点懵了是吧,我画了张不太准确的图帮助理解

oauth2-proxy大致的流程图

kubectl edit configmap -n istio-system istio 编辑istio的配置,最后大概长这样,注意里面的注释说明

apiVersion: v1
data:
  mesh: |-
    defaultConfig:
      discoveryAddress: istiod.istio-system.svc:11111
      proxyMetadata: {}
    enablePrometheusMerge: true
    rootNamespace: istio-system
    trustDomain: cluster.local
    # 上面的都是原来的配置,不要改,关键是下面这个
    extensionProviders:
    - name: "gh-example-oauth2" # 要记得这个名字,等会儿会用到
      envoyExtAuthzHttp:
        # 这里要指向你自己的oauth2-proxy安装的service,我这里是放在oauth2-proxy这个namespace下
        service: "gh-oauth2-proxy.oauth2-proxy.svc.cluster.local"
        port: "80" # 这个是helm安装的默认端口(kubeapp也是)
        includeRequestHeadersInCheck: ["authorization", "cookie"] # 这三个必须和我的一样
        headersToUpstreamOnAllow: ["authorization", "path", "X-Auth-Request-User", "X-Auth-Request-Email", "X-Auth-Request-Access-Token"]
        headersToDownstreamOnDeny: ["content-type", "set-cookie"]

然后再配置istion的virtual service,让服务整个跑起来,同样注意我里面的注释,官方的文档写得太文绉绉,喜欢的也可以去读一下(链接)

apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: example-vs
  namespace: example-ns #应用自己的namespace
spec:
  hosts:
  - "jaeger.example.com"
  gateways:
  - example-gw # 之前安装istio时的gateway
  http:
  - match: # 这里保持和github里的一致
    - uri:
        prefix: /oauth2
    route:
    - destination:
        host: gh-oauth2-proxy.oauth2-proxy.svc.cluster.local # oauth2的安装svc地址
        port:
          number: 80
  - match:
    - uri:
        prefix: /
    route:
    - destination:
        host: jaeger.app.svc.cluster.local # jaeger的svc地址
        port:
          number: 8080
---
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
  name: jaeger-github-oauth2
  namespace: example-ns
spec:
  selector:
    matchLabels:
      app.kubernetes.io/component: query #重要!不要填错了,匹配不上不会生效的
      app.kubernetes.io/name: jaeger
  action: CUSTOM
  provider:
    name: "gh-example-oauth2" # 跟istio配置里的extensionProviders保持一致
  rules:
  - to:
    - operation:
        # 注意是精确匹配的!!要加通配符才能前缀匹配
        paths: [ "/*"]      

kubectl apply之后,重启istio,访问你的应用,看到这个就成功啦~

参考: https://medium.com/@lucario/istio-external-oidc-authentication-with-oauth2-proxy-5de7cd00ef04

k8s配置istio并自动全站https

AWS的k8s(Kubernetes)需要安装一个istio来方便管理servicemesh、动态调整流量、附加https等

官网的安装说明已经很不错了,我个人喜欢用helm部署,以下是helm 3.6以上部署方式

helm repo add istio https://istio-release.storage.googleapis.com/charts
kubectl create namespace istio-system
helm install istiod istio/istiod -n istio-system --wait

# 这里开始就和官网不一样了,直接安装gateway到 istio-system里,这样方便整体删除
helm install istio-ingressgateway istio/gateway -n istio-system --wait

这时,AWS会自动分配一个ELB给你

istio自动添加ELB

到这里istio安装就完成了,接下来是cert-manager,我喜欢用kubeapp 进行管理,就可以一键安装cert-manager(个人偏好换了个namespace)

安装完毕后,开始配置cert-manager, kubectl apply 走起~

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod-cluster #注意这个名字
  namespace: istio-system
spec:
  acme:
    email: xxx@mzh.io
    server: https://acme-v02.api.letsencrypt.org/directory # 正式环境的地址,stage的可以看
    privateKeySecretRef:
      name: letsencrypt-prod-cluster
    solvers:
    - http01:
        ingress:
          class: istio

---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: example-cert
  namespace: istio-system
  annotations:
    cert-manager.io/issue-temporary-certificate: "true" # 防止配置错误一直取不到证书
spec:
  secretName: example-cert #最好跟name一致
  isCA: false
  usages:
    - server auth
    - client auth
  issuerRef:
    name: letsencrypt-prod-cluster #要和前面的ClusterIssuer里的名字一样
    kind: ClusterIssuer
    group: cert-manager.io
  dnsNames:
    - example.com # 这里换成你自己的域名

现在来配置istio对应的站点, kubectl apply 继续走起~

apiVersion: networking.istio.io/v1beta1
kind: Gateway
metadata:
  name: example-gw
  namespace: default
spec:
  selector:
    istio: ingressgateway #这里就是helm安装的时候那个app名字,去掉istio-
  servers:
  - port:
      number: 80
      name: http
      protocol: HTTP
    hosts:
    - example.com
  - port:
      number: 443
      name: https
      protocol: HTTPS
    tls:
      httpsRedirect: false #防止配置错误乱跳
      mode: SIMPLE
      credentialName: example-cert #跟刚才的Certificate里的一样
    hosts:
    - example.com

---
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: example
  namespace: default
spec:
  hosts:
  - "example.com"
  gateways:
  - example-gw # 跟前面的Gateway名字一致
  http:
  - name: main-site
    route:
    - destination:
        host: site.default.cluster.local # K8S集群里的名字,当然可以看官方按app和version配置,不过这次简单点
        port:
          number: 8080

接下来配置对应的DNS记录,我这里用了Route53,注意可以直接用alias指向ELB,因为ELB的地址会换IP,所以不要轻易用A记录直接指向。

AWS配置ELB alias

好了~这下配置完成了,但是还是连不上怎么办?

debug最好用的是istioctl dashboard kiali ,可以很直观的看到哪里配置错误,按照里面的提示修改就行。

官方教程 https://istio.io/latest/docs/tasks/observability/kiali/

ESXi安装k8s集群

最近ESXi 虚拟机安装完毕,开始折腾k8s集群。

k8s 集群拓扑

我先安装了Debian 11(bullseye),但是里面的软件比如containerd都很老了,只能跑1.4.3没办法设定指定镜像,换成Debian 12和Ubuntu 22.04都有网络没办法互通的问题……怎么改iptables都不行,只好用Fedora 36。这下才终于成功了,下面记录一下我遇到的坑。

首先对某墙只想唱:“听我说,谢谢你”,大家安装过程可以用阿里云的镜像。官方文档https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/install-kubeadm/ 其实已经非常全面和详细了,不要漏了任何一步,我就漏了加载overlay 模块导致的集群起不来的尴尬……

给路由器配置了自动DHCP,这样局域网里可以直接用fed-k8s-master这种域名访问了。

Fedora登入后第一步就是设置机器名称

hostnamectl set-hostname fed-k8s-master

然后是系统配置,比如必要的内核模块,sysctl一些东西,fedora还有个问题,就是要关掉防火墙

cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF

sudo modprobe overlay
sudo modprobe br_netfilter

# sysctl params required by setup, params persist across reboots
cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-iptables  = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward                 = 1
EOF

# Apply sysctl params without reboot
sudo sysctl --system
sudo systemctl stop firewalld && sudo systemctl disable firewalld
sudo dnf remove zram-generator-defaults

# Set SELinux in permissive mode (effectively disabling it)
sudo setenforce 0
sudo sed -i 's/^SELINUX=enforcing$/SELINUX=permissive/' /etc/selinux/config

接着配置k8s fedora源,记得把里面的proxy换成你科学上网用的(用aliyun的也问题不大,就是我不太喜欢……)

cat <<EOF | sudo tee /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://packages.cloud.google.com/yum/repos/kubernetes-el7-\$basearch
enabled=1
gpgcheck=1
gpgkey=https://packages.cloud.google.com/yum/doc/yum-key.gpg https://packages.cloud.google.com/yum/doc/rpm-package-key.gpg
exclude=kubelet kubeadm kubectl
proxy=<你的代理地址>
EOF

sudo yum install -y kubelet kubeadm kubectl containerd iproute-tc --disableexcludes=kubernetes
sudo systemctl enable --now kubelet containerd

接下来就是配置containerd,这里有3个坑:

  • 一个是cgroup driver要配置成systemd
  • runtime 的type 得手动配置
  • 还有就是坑爹的sandbox镜像源(可以理解成占位)

最后使用的配置文件就是:/etc/containerd/config.toml

version = 2

[plugins]
  [plugins."io.containerd.grpc.v1.cri"]
    sandbox_image = "registry.aliyuncs.com/google_containers/pause:3.6"
    [plugins."io.containerd.grpc.v1.cri".cni]
      bin_dir = "/usr/libexec/cni/"
      conf_dir = "/etc/cni/net.d"
  [plugins."io.containerd.internal.v1.opt"]
    path = "/var/lib/containerd/opt"
  [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
    runtime_type = "io.containerd.runc.v2"
    [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
      SystemdCgroup = true

开始初始化master control-plane(控制面主机),10.0.0.39 改成你自己的主机ip

kubeadm init --apiserver-advertise-address 10.0.0.39 --image-repository registry.aliyuncs.com/google_containers

执行成功会返回一个token

kubeadm join 10.0.0.39:6443 --token xlstub.uvzof5s4mz504ev1 \
--discovery-token-ca-cert-hash sha256:9cc767e5d1e2f2707d4e1f5a1270c569aeca3a49185aa591a9d2142f8d352198

先拷下来,然后不管他,因为我们还需要CNI(container network interface),简单来说就是容器间互相访问的网络, 我这里选了flannel,下载flanneld二进制,并应用配置

mkdir -p /opt/bin && wget https://github.com/flannel-io/flannel/releases/download/v0.19.0/flanneld-amd64 -O /opt/bin/flanneld
kubectl apply -f https://raw.githubusercontent.com/flannel-io/flannel/master/Documentation/kube-flannel.yml

这里也有个坑:不管是coredns还是flanneld都一直起不来。其实就是k8s的一个bug……效果是

[root@fed-k8s-master ~]# kubectl get pods --all-namespaces
NAMESPACE      NAME                                     READY   STATUS              RESTARTS        AGE
kube-flannel   kube-flannel-ds-95sv7                    0/1     CrashLoopBackOff    7 (3m52s ago)   15m
kube-flannel   kube-flannel-ds-qjjcn                    0/1     CrashLoopBackOff    7 (3m36s ago)   15m
kube-system    coredns-74586cf9b6-fkd5x                 0/1     ContainerCreating   0               79m
kube-system    coredns-74586cf9b6-ktvk7                 0/1     ContainerCreating   0               79m
kube-system    etcd-fed-k8s-master                      1/1     Running             0               79m
kube-system    kube-apiserver-fed-k8s-master            1/1     Running             0               79m
kube-system    kube-controller-manager-fed-k8s-master   1/1     Running             0               79m
kube-system    kube-proxy-cl2l6                         1/1     Running             0               79m
kube-system    kube-proxy-llln9                         1/1     Running             0               30m
kube-system    kube-scheduler-fed-k8s-master            1/1     Running             0               79m

要是crictl logs 看为啥起不来,就会发现:

I0721 03:54:55.082767       1 match.go:206] Determining IP address of default interface
I0721 03:54:55.083292       1 match.go:259] Using interface with name ens192 and address 10.0.0.39
I0721 03:54:55.083350       1 match.go:281] Defaulting external address to interface address (10.0.0.39)
I0721 03:54:55.083485       1 vxlan.go:138] VXLAN config: VNI=1 Port=0 GBP=false Learning=false DirectRouting=false
E0721 03:54:55.084170       1 main.go:330] Error registering network: failed to acquire lease: node "fed-k8s-master" pod
 cidr not assigned
I0721 03:54:55.084272       1 main.go:447] Stopping shutdownHandler...
W0721 03:54:55.084438       1 reflector.go:436] github.com/flannel-io/flannel/subnet/kube/kube.go:403: watch of *v1.Node
 ended with: an error on the server ("unable to decode an event from the watch stream: context canceled") has prevented
the request from succeeding

这是个k8s的bug !,需要手动调整/etc/kubernetes/manifests/kube-controller-manager.yaml 这个文件,加上这两个选项……

--allocate-node-cidrs=true
--cluster-cidr=10.244.0.0/16

重启kubelet,master就配置好了。

在另一台一样配置好的fedora机器上,执行刚才的join命令,这样,一个简单的集群就配置好了

在Hifive unmatched上安装FreeBSD

最近手痒想试试FreeBSD,正好FreeBSD ports 也在找一台builder,吴老板给我凑了2台Unmatched 跑Linux的Go builder,所以我这台就空出来了~

其实按着官方的教程就好,官方教程:https://wiki.freebsd.org/riscv/HiFiveUnmatched

不过要注意2点!!(花了我两个晚上),一切的错误都从我没仔细看教程开始……第一个错误是直接把FreeBSD的镜像直接烧进了SD卡里!然后以为这就行了~结果发现,其实Unmatched的固件决定了必须从SD上特定的分区才能启动!所以直接烧进SD卡里,应该是根据教程写进一个U盘里。

也就是一共需要三个东西:

  1. 装有原生镜像的sd卡
  2. dd有FreeBSD memstick image的U盘
  3. 一个空的nvme (三星970最好)

诡异的是,重新给SD卡烧录了Ubuntu的镜像,系统nvme就因为找不到root直接进(initramfs)了。

我脑子抽了就直接用SD卡里的Ubuntu shell 直接 dd if=/dev/zero of=/dev/nvme0n1 …… 想通过干掉分区表(GPT)来阻止从nvme启动。结果噩梦就开始了,SD卡里的Ubuntu怎么都找不到nvme和U盘了。折腾了一晚没啥收获,第二天不停的重启和对比才发现。UBoot启动的过程中需要启动pci!要不然你怎么执行命令启动USB都是不行的,usb start 就会报错 no working controllers found

所以正确的做法是,在U-boot自动启动过程中先启动pci 子系统

pci enum
setenv boot_targets usb0
boot

最后就是标准的FreeBSD安装流程了~ yes~

Go团队如何解bug[1]: 乱序执行与内存屏障

开篇前言

Go项目在发展过程中有过许许多多的bug,核心开发组在解决bug的时候有各种各样的方法,涉及的知识点都很值得学习。
我觉得这些都挺值得跟大家分享,也希望做成一个不鸽系列。

正题

Issue地址:issues/35541

开发:Cherry Zhang, Michael Knyszek

这个bug是由Go的自动构建机器linux-mipsle-mengzhuo在日常测试过程中首次发现的,
表现是某个对象里的指针指向了一个不存在的span上,导致runtime panic。

Cherry根据 经验 判断出这个span类型是goroutine用的,并且贴出了对应的对象映射(如下)

object=0xc000106300 s.base()=0xc000106000 s.limit=0xc000107f80 s.spanclass=42 s.elemsize=384 s.state=mSpanInUse
*(object+0) = 0xc00052a000 g.stack.lo
*(object+8) = 0xc000532000 g.stack.hi
*(object+16) = 0xc00052a380 g.stackguard0 = g.stack.lo + _Stackguard = g.stack.lo + 896
*(object+24) = 0xffffffffffffffff g.stackguard1
*(object+32) = 0x0 g._panic
*(object+40) = 0xc0005313e0 <== // 故障点 g._defer
*(object+48) = 0xc000080380 g.m
*(object+56) = 0xc0005310b8 g.sched.sp
*(object+64) = 0x87038 g.sched.pc
*(object+72) = 0xc000106300 g.sched.g

虽然Cherry没有明说如何判断出是goroutine,但其中的偏移量24字节处的数据正好是64位的满位,
看起来确实像一般的goroutine的stackguard1 (malg时分配的)

这个bug奇怪的地方在于不是必现,是在不同的函数,时不时runtime panic, 而且出问题的span有时是在用的,有时又是废弃的。
更奇怪的是,RTRK(塞尔维亚的一家科研机构)提供的MIPS构建机从来都没有出现过一次类似原因的runtime panic。

一开始,开发组认为是MIPS的问题,时间长了,发现还有plan9-arm,
darwin-arm64-corellium这两个架构也出现了类似问题,然而出现的频率远不如MIPS高。
因为没有有效解,几乎每周Bryan Mills(负责Go开发质量的工程师)都会在Github Issue中填上新增的panic日志地址,最后他也嫌烦了……

脑袋疼.jpg

问题定位

事情的转机是我有一次debug过程中,发现runtime有个测试函数TestDeferHeapAndStack 总是 会引发这个panic。
而在Go项目的自动化整体测试中,为了测试尽快完成,通常速度慢的机器都会选择short mode,
而恰好这个short mode不会导致panic。

本身这个测试也很有意思,因为编译器只会把函数作用域中无指针的defer分配在stack上,而循环体中的defer就会分配在heap中。

测试用例如下:

func deferHeapAndStack(n int) (r int) {
if n == 0 {
return 0
}
if n%2 == 0 {
// heap-allocated defers
for i := 0; i < 2; i++ {
defer func() {
r++
}()
}
} else {
// stack-allocated defers
defer func() {
r++
}()
defer func() {
r++
}()
}
r = deferHeapAndStack(n - 1)
escapeMe(new([1024]byte)) // force some GCs
return
}

我尝试着自己修复这个bug,然而失败了,不过既然可以稳定重现,那可以帮助核心团队来bisect这个bug。

这个技能是从Austin(Go runtime leader)学来的(他如何从Go定位Linux bug正好可以写下一期哈)。

因为这个bug是从1.13之后才有的,所以我选择了HEAD 和 go1.13.9之间开始定位。

$ git bisect HEAD go1.13.9
$ git bisect run ./test.sh // 就是跑上面deferTest的脚本

最后发现是runtime: rearrange mheap_.alloc* into allocSpan这个commit导致了bug。

修复方法

Cherry在和Michael聊这个bug之后发现在allocSpan的过程中由于缺了一个内存屏障(memory barrier),
在弱内存顺序模型的机器上这个bug会随机出现。以下是她的修复commit:

When copying a stack, we
1. allocate a new stack,
2. adjust pointers pointing to the old stack to pointing to the
new stack.
If the GC is running on another thread concurrently, on a machine
with weak memory model, the GC could observe the adjusted pointer
(e.g. through gp._defer which could be a special heap-to-stack
pointer), but not observe the publish of the new stack span. In
this case, the GC will see the adjusted pointer pointing to an
unallocated span, and throw. Fixing this by adding a publication
barrier between the allocation of the span and adjusting pointers.
One testcase for this is TestDeferHeapAndStack in long mode. It
fails reliably on linux-mips64le-mengzhuo builder without the fix,
and passes reliably after the fix.

简单翻译一下:

当拷贝一个栈数据(stack)时,我们先:
1. 申请一个新的栈空间
2. 将老的栈上的指针(pointer)从老栈指向新栈上。
如果此时在另一个线程并行地执行着GC(垃圾回收),且架构是弱内存顺序模型时,GC可以观察到指针的调整
(例如: gp._defer 就是一个特殊的 heap-to-stack 指针),但是观察不到新的栈分配。
这时GC就会观察到指针分配到了一个没有分配的空间中。
我们通过在分配和调整指针之间添加内存屏障(memory barrier)来修复这个bug。

代码就更简单了,只是在allocSpan的过程中添加了一个函数publicationBarrier()

什么是内存顺序模型可以参考这个系列的文章《Memory Barriers Are Like Source Control Operations》

X86没有问题,是因为强内存模型保证了每个写入操作的顺序能在各个CPU之间保证同步。
而弱内存模型就没有保证这点,所以强制sync

等等,你问为啥ARM没有这个问题?这是玄学(划掉
因为ARM虽然是弱内存模型,但是保证了“Data dependency barrires

所以以后碰到诡异的多线程问题,就先sync(手动狗头

那么什么是内存顺序模型?

咕咕咕……回头再补

多线程问题

MESI

小结

参考资料

[^1] https://blog.golang.org/ismmkeynote
[^2] https://www.ardanlabs.com/blog/2018/12/garbage-collection-in-go-part1-semantics.html
[^3] https://zhuanlan.zhihu.com/p/48157076