LOADING

加载过慢请开启缓存 浏览器默认开启

论远程零信任访问的搭建(1)Headscale的搭建

前言

在迁移AquaDX到K8s集群中的时候,我需要给别人远程访问的权限

我的服务器是不公开的 我并不打算公开出去给所有人用

之前用的ZeroTier因为几个原因我决定废弃了:

  • 他把路由选项从控制面板中删除了,改为要付费的
  • 他要修改宿主机的tun配置来实现二层上的网络代理,这在K8s中是不允许的
  • 打洞策略太迷 即使能UPnP直连或者IPv6直连有时候他还是中转

第一点导致我没法用来代理整个网段了,以前我把他装在OpenWrt上,这样我可以连接上网络之后访问我整个局域网,现在没了

第二点则是他的硬伤,或许我们有方法让他不修改tun,但显然没必要

我们可以选择性能更高的三层网络方案,基于WireGuard的一大堆都可以

我看到了Github上的这个:https://github.com/HarvsG/WireGuardMeshes/blob/main/readme.md

Code_qMmZJDTt8C.png

可以看到最成熟的应该是NetBird和Tailscale

我在之前是使用过Tailscale的,也确实很成熟

NetBird也不差

考虑到我用过Tailscale,我决定还是用他

但是Tailscale打洞中转延迟非常高,因为国内没有服务器

那么有没有最夯的方式呢

自建Headscale

我其实想过自建Netbird,但我一打开Netbird那网站和Tailscale也差不多嘛

上面那个图的HeadScale其实就是Tailscale控制平面的开源实现

TailScale客户端是开源的,控制端则闭源 和Zerotier一样

事实上的话我们可以自己搭建中继节点,然后用Tailscale官方的功能就完全足够了

但是都要自己搭了我干嘛不把控制平面也自己弄了呢()顺便解锁一些收费才能用的功能

也能同时解决打不了洞难中转的问题

其实的话不难,Headscale麻烦的是那个config

---
apiVersion: apps/v1
kind: Deployment
metadata:
  annotations: {}
  labels:
    app: headscale
    k8s.kuboard.cn/name: headscale
  name: headscale
  namespace: headscale
spec:
  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: v1
kind: Service
metadata:
  annotations:
    metallb.io/ip-allocated-from-pool: default-address-pool
  labels:
    app: headscale
    k8s.kuboard.cn/name: headscale
  name: headscale
  namespace: headscale
spec:
  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: LoadBalancer
status:
  loadBalancer:
    ingress:
      - ip: 10.0.100.11
        ipMode: VIP

然后的话是config

server_url: http://127.0.0.1:8080

listen_addr: 0.0.0.0:8080
metrics_listen_addr: 0.0.0.0:9090
grpc_listen_addr: 0.0.0.0:50443
grpc_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: false
ephemeral_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/directory
tls_letsencrypt_cache_dir: /var/lib/headscale/cache
tls_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.sock
unix_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

chrome_HxIF1PpUZy.png

不管怎么说,我们先把他分开部署

请把Plane和Scale放在同一个Deployment中,详情看后面就知道了😄

---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    k8s.kuboard.cn/name: plane
  name: plane
  namespace: headscale
spec:
  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: v1
kind: Service
metadata:
  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/v1
kind: Ingress
metadata:
  annotations: {}
  labels:
    k8s.kuboard.cn/name: plane
  name: plane
  namespace: headscale
spec:
  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段删了,我们先跑起来再去配置

然后就报错了

chrome_o0shplXi5G.png

傻逼了 因为我们设置了kubernetes integration

简单来说就是可以让Plane自动发现集群中的Scale服务,自动通信

我们现在需要设置一下Service Account,默认的default是没有任何权限的

我们需要让plane可以查看访问同命名空间下的pod

---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: headplane
  namespace: headscale

---

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: headplane-role
  namespace: headscale
rules:
  - apiGroups: [""]
    resources: ["pods"]
    verbs: ["get", "list", "watch"]

---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: headplane-role
  namespace: headscale
rules:
  - apiGroups: [""]
    resources: ["pods"]
    verbs: ["get", "list", "watch"]

然后修改一下Plane让他使用这个ServiceAccount

chrome_yEXT2ASdGI.png

重启一下

chrome_aWvroyqhBG.png

尼玛。

chrome_7tRqUk9g66.png

我想到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"

我去看看他的文档先:

chrome_cVrkDNWzro.png

chrome_4tnEd6YmDo.png

chrome_aOqefgYEOX.png

?你这个文档在哪呢

反正说白了就是,Pod名字是会变动的,我们可以用Downward API去解决这个问题

等一下 席巴了 通过这个注入podname需要同一个Deployment

chrome_bdyAWlASSu.png

我真是草了

其实也不用说非得注入,我们可以把Scale改成StatefulSet,这样他的名字就是固定的了

其实的话integration是可以false的,我们在上面已经通过集群内域名实现Plane对Scale的访问了

但我已经折腾到这了 给他一起配上吧

把Plane也放到同一个Deployment里作为另一个工作容器

等一下啊 configmap是只读的

那怎么让Plane修改config

chrome_7tRqUk9g66.png

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

卧槽我们跑起来还要跑Sidecar呢

chrome_f2YiWJ9496.png

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

chrome_VmyRTHeNjl.png

chrome_0ezfV0GyRt.png

欸等一下

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

chrome_W6NFyj0mmt.png

白浪费一个小时去来回调,先用Service访问不香吗(

那我们进headscale容器生成一个key先

这headscale容易默认啥shell都没有

我们还得去master节点exec一下

MobaXterm_HhxL4xJ142.png

忘记指定命名空间了 真玩傻了

kubectl exec -n headscale -it headscale-59bd5c7d65-8j5k6 -- headscale apikeys create

然后就可以进到管理界面了

那就剩一个小问题

chrome_D5PlYdJGjl.png

我们得把Configmap拷贝到PVC中并允许多节点同时读写

我选择用一个init pod自动拷贝,这样就能解决问题了

chrome_tK9Qo4sPEJ.png

chrome_hO3RyeKrJP.png

然后就在这时候

MobaXterm_Uma9Js4Akw.png

Lens_H0cPn9el6f.png

卧槽Worker炸了

真是坎坎又坷坷啊

我一看 普罗米修斯在我的Worker里给人类偷取火种去了

MobaXterm_yXuv6WzAlj.png

西巴了搞个wg组网搞一晚上

我先把Grafana监控套件全删了

这个是题外话 修好之后就可以正常使用headplane了

chrome_h8OGPj5A47.png

写了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加密的

chrome_4vfiXv3hhx.png

这样的话就可以实现加入网络和管理都用同一套帐号了

完事 接下来是Sidecar 太长了我决定放到另一篇