论远程零信任访问的搭建(1)Headscale的搭建
前言
在迁移AquaDX到K8s集群中的时候,我需要给别人远程访问的权限
我的服务器是不公开的 我并不打算公开出去给所有人用
之前用的ZeroTier因为几个原因我决定废弃了:
- 他把路由选项从控制面板中删除了,改为要付费的
- 他要修改宿主机的tun配置来实现二层上的网络代理,这在K8s中是不允许的
- 打洞策略太迷 即使能UPnP直连或者IPv6直连有时候他还是中转
第一点导致我没法用来代理整个网段了,以前我把他装在OpenWrt上,这样我可以连接上网络之后访问我整个局域网,现在没了
第二点则是他的硬伤,或许我们有方法让他不修改tun,但显然没必要
我们可以选择性能更高的三层网络方案,基于WireGuard的一大堆都可以
我看到了Github上的这个:https://github.com/HarvsG/WireGuardMeshes/blob/main/readme.md

可以看到最成熟的应该是NetBird和Tailscale
我在之前是使用过Tailscale的,也确实很成熟
NetBird也不差
考虑到我用过Tailscale,我决定还是用他
但是Tailscale打洞中转延迟非常高,因为国内没有服务器
那么有没有最夯的方式呢
自建Headscale
我其实想过自建Netbird,但我一打开Netbird那网站和Tailscale也差不多嘛
上面那个图的HeadScale其实就是Tailscale控制平面的开源实现
TailScale客户端是开源的,控制端则闭源 和Zerotier一样
事实上的话我们可以自己搭建中继节点,然后用Tailscale官方的功能就完全足够了
但是都要自己搭了我干嘛不把控制平面也自己弄了呢()顺便解锁一些收费才能用的功能
也能同时解决打不了洞难中转的问题
其实的话不难,Headscale麻烦的是那个config
---apiVersion: apps/v1kind: Deploymentmetadata: annotations: {} labels: app: headscale k8s.kuboard.cn/name: headscale name: headscale namespace: headscalespec: replicas: 1 selector: matchLabels: app: headscale labels: app: headscale spec: containers: - command: - headscale - serve image: 'docker.io/headscale/headscale:stable' imagePullPolicy: IfNotPresent name: headscale ports: - containerPort: 8080 name: web protocol: TCP - containerPort: 9090 name: metrics protocol: TCP - containerPort: 50443 name: grpc protocol: TCP volumeMounts: - mountPath: /var/lib/headscale name: headscale-data - mountPath: /var/run/headscale name: headscale-data - mountPath: /etc/headscale name: config readOnly: true volumes: - name: headscale-data persistentVolumeClaim: claimName: headscale-data - configMap: defaultMode: 420 items: - key: server-config.yaml path: config.yml name: server-config.yaml name: config---apiVersion: v1kind: Servicemetadata: annotations: metallb.io/ip-allocated-from-pool: default-address-pool labels: app: headscale k8s.kuboard.cn/name: headscale name: headscale namespace: headscalespec: allocateLoadBalancerNodePorts: true clusterIP: 10.110.187.183 clusterIPs: - 10.110.187.183 externalTrafficPolicy: Cluster internalTrafficPolicy: Cluster ipFamilies: - IPv4 ipFamilyPolicy: SingleStack ports: - name: web nodePort: 30667 port: 8080 protocol: TCP targetPort: 8080 - name: metrics nodePort: 32383 port: 9090 protocol: TCP targetPort: 9090 - name: grpc nodePort: 32594 port: 50443 protocol: TCP targetPort: 50443 selector: app: headscale sessionAffinity: None type: LoadBalancerstatus: loadBalancer: ingress: - ip: 10.0.100.11 ipMode: VIP然后的话是config
server_url: http://127.0.0.1:8080
listen_addr: 0.0.0.0:8080metrics_listen_addr: 0.0.0.0:9090grpc_listen_addr: 0.0.0.0:50443grpc_allow_insecure: true
noise: private_key_path: /var/lib/headscale/noise_private.key
prefixes: v4: 100.64.0.0/10 v6: fd7a:115c:a1e0::/48 allocation: sequential
derp: server: enabled: false urls: - https://controlplane.tailscale.com/derpmap/default auto_update_enabled: true update_frequency: 3h
disable_check_updates: falseephemeral_node_inactivity_timeout: 30m
database: type: sqlite sqlite: path: /var/lib/headscale/db.sqlite write_ahead_log: true wal_autocheckpoint: 1000
acme_url: https://acme-v02.api.letsencrypt.org/directorytls_letsencrypt_cache_dir: /var/lib/headscale/cachetls_cert_path: ""tls_key_path: ""
log: level: info format: text
policy: mode: file path: ""
dns: magic_dns: true base_domain: headscale.lan override_local_dns: true nameservers: global: - 1.1.1.1 - 8.8.8.8 split: {} search_domains: [] extra_records: []
unix_socket: /var/run/headscale/headscale.sockunix_socket_permission: "0770"
logtail: enabled: false
randomize_client_port: false不出意外就没问题了,但是Headscale没有GUI,搭起来之后我们得用CLI访问
这行吗 这肯定不行啊
我们可以参考Headscale的官方界面选一个GUI
https://headscale.net/stable/ref/integration/web-ui/
我选了一下有四个
https://github.com/simcu/headscale-ui
https://github.com/tale/headplane
https://github.com/GoodiesHQ/headscale-admin
https://github.com/rickli-cloud/headscale-console
根据自己对UI的喜好选一个就好
最后我根据Star数和更新频率决定选择headplane
他的UI也是仿造的Tailscale 把人饭碗抢光光了
也好,熟悉Tailscale可以直接上手
去看一下这玩意还支持反代,SSO,SSH key 完美
看了一下为了Headplane能管理,最好的方式是把他们跑在一起
我一开始想的是给他们挂在同一个Deployment下 使用两个工作容器
其实没必要,分开也是完全可以的
因为我把headscale和headplane看混好几次
这他妈也太像了吧
为了便于区分,之后Headscale简称Scale,Headplane简称Plane

不管怎么说,我们先把他分开部署
请把Plane和Scale放在同一个Deployment中,详情看后面就知道了😄
---apiVersion: apps/v1kind: Deploymentmetadata: labels: k8s.kuboard.cn/name: plane name: plane namespace: headscalespec: progressDeadlineSeconds: 600 replicas: 1 revisionHistoryLimit: 2 selector: matchLabels: k8s.kuboard.cn/name: plane template: metadata: labels: k8s.kuboard.cn/name: plane spec: containers: - image: 'ghcr.io/tale/headplane:latest' imagePullPolicy: IfNotPresent name: headplane ports: - containerPort: 3000 name: web protocol: TCP volumeMounts: - mountPath: /etc/headplane name: config - mountPath: /var/lib/headplane name: data - mountPath: /etc/headscale name: scale-config readOnly: true volumes: - configMap: items: - key: config.yml path: config.yaml name: plane-config name: config - name: data persistentVolumeClaim: claimName: headplane-data - configMap: items: - key: server-config.yaml path: config.yaml name: server-config.yaml name: scale-config
---apiVersion: v1kind: Servicemetadata: annotations: {} labels: k8s.kuboard.cn/name: plane name: plane namespace: headscale ports: - name: web port: 3000 protocol: TCP targetPort: 3000 selector: k8s.kuboard.cn/name: plane sessionAffinity: None type: ClusterIP
---apiVersion: networking.k8s.io/v1kind: Ingressmetadata: annotations: {} labels: k8s.kuboard.cn/name: plane name: plane namespace: headscalespec: ingressClassName: nginx-ingress rules: - host: headplane.k8s.lan http: paths: - backend: service: name: plane port: number: 3000 path: / pathType: Prefix然后还有Config
server: host: "0.0.0.0" port: 3000 cookie_secret: "$(openssl rand -hex 16)一个在这里" cookie_secure: false # 不改的话不是https不会发送Cookie data_path: "/var/lib/headplane" headscale: url: "http://headscale.headscale.svc.cluster.local" config_path: "/etc/headscale/config.yaml" config_strict: true integration: agent: enabled: false pre_authkey: "<your-preauth-key>" docker: enabled: false container_label: "me.tale.headplane.target=headscale" socket: "unix:///var/run/docker.sock" kubernetes: enabled: true validate_manifest: true pod_name: "headscale"
proc: enabled: false我这里把OIDC段删了,我们先跑起来再去配置
然后就报错了

傻逼了 因为我们设置了kubernetes integration
简单来说就是可以让Plane自动发现集群中的Scale服务,自动通信
我们现在需要设置一下Service Account,默认的default是没有任何权限的
我们需要让plane可以查看访问同命名空间下的pod
---apiVersion: v1kind: ServiceAccountmetadata: name: headplane namespace: headscale
---
apiVersion: rbac.authorization.k8s.io/v1kind: Rolemetadata: name: headplane-role namespace: headscalerules: - apiGroups: [""] resources: ["pods"] verbs: ["get", "list", "watch"]
---apiVersion: rbac.authorization.k8s.io/v1kind: Rolemetadata: name: headplane-role namespace: headscalerules: - apiGroups: [""] resources: ["pods"] verbs: ["get", "list", "watch"]然后修改一下Plane让他使用这个ServiceAccount

重启一下

尼玛。

我想到Pod名字是随机的,我们在Config里填的
kubernetes: enabled: true validate_manifest: true pod_name: "headscale"他其实还有一行注释在上面:
kubernetes: enabled: true validate_manifest: true # This should be the name of the Pod running Headscale and Headplane. # If this isn't static you should be using the Kubernetes Downward API # to set this value (refer to docs/Integrated-Mode.md for more info). pod_name: "headscale"我去看看他的文档先:



?你这个文档在哪呢
反正说白了就是,Pod名字是会变动的,我们可以用Downward API去解决这个问题
等一下 席巴了 通过这个注入podname需要同一个Deployment

我真是草了
其实也不用说非得注入,我们可以把Scale改成StatefulSet,这样他的名字就是固定的了
其实的话integration是可以false的,我们在上面已经通过集群内域名实现Plane对Scale的访问了
但我已经折腾到这了 给他一起配上吧
把Plane也放到同一个Deployment里作为另一个工作容器
等一下啊 configmap是只读的
那怎么让Plane修改config

我真红了 我写到这已经边折腾边写3小时了 直接用Tailscale的话Sidecar都跑起来收工了
卧槽我们跑起来还要跑Sidecar呢

没话说了 折腾到这我已经有点沉默了 我们先不管integration 先看看能不能用


欸等一下
给自己折腾傻了,Plane的默认界面是/admin

白浪费一个小时去来回调,先用Service访问不香吗(
那我们进headscale容器生成一个key先
这headscale容易默认啥shell都没有
我们还得去master节点exec一下

忘记指定命名空间了 真玩傻了
kubectl exec -n headscale -it headscale-59bd5c7d65-8j5k6 -- headscale apikeys create然后就可以进到管理界面了
那就剩一个小问题

我们得把Configmap拷贝到PVC中并允许多节点同时读写
我选择用一个init pod自动拷贝,这样就能解决问题了


然后就在这时候


卧槽Worker炸了
真是坎坎又坷坷啊
我一看 普罗米修斯在我的Worker里给人类偷取火种去了

西巴了搞个wg组网搞一晚上
我先把Grafana监控套件全删了
这个是题外话 修好之后就可以正常使用headplane了

写了500多行了 终于完事了
那我们就顺便配置下OIDC吧
只需要在Plane的下面加上这些就好了
Scale同理,这样的话登录的时候就不用去手动输入Machine key,过OIDC就好了
oidc: issuer: "https://example.com/application/o/headplane-oidc/" client_id: "secret" client_secret: "secret" scope: "openid email profile" disable_api_key_login: true token_endpoint_auth_method: "client_secret_post" headscale_api_key: "api key" redirect_uri: "https://example.com/admin/oidc/callback"Authentik上的话,别的都和正常创建OIDC一样,最关键的是要选择一个签名证书,并且是RS256加密的

这样的话就可以实现加入网络和管理都用同一套帐号了
完事 接下来是Sidecar 太长了我决定放到另一篇
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!