Compare commits
35
Commits
9360eb2ea0
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a729f01546 | ||
|
|
bbcd5d6aa5
|
||
|
|
c0c4ff521b | ||
|
|
8bc039b40f | ||
|
|
349aa77fb0
|
||
|
|
cae6acbfb0 | ||
|
|
4d80f8efd7
|
||
|
|
9009e3fa11 | ||
|
|
a252ee5602 | ||
|
|
dbf66590ac | ||
|
|
e8e7bd4b20 | ||
|
|
185e3edaba | ||
|
|
072936ce03 | ||
|
|
39aeac25ee | ||
|
|
a43b7d3c26 | ||
|
|
c06c6f7857 | ||
|
|
6a27adc18d | ||
|
|
29b3bdeeb1 | ||
|
|
7a62dde601 | ||
|
|
521a49599a | ||
|
|
185a47e2bb | ||
|
|
8dabbdce68 | ||
|
|
d5b1bb9640 | ||
|
|
cfb2fddfa9
|
||
|
|
7f218531ed | ||
|
|
8f3d1ed04a
|
||
|
|
5435d7d2d5
|
||
|
|
e325a703dd | ||
|
|
130849040c
|
||
|
|
bcd034355e | ||
|
|
506e0cb983
|
||
|
|
c07709e078 | ||
|
|
e1979ebbb8
|
||
|
|
b02b2aec96 | ||
|
|
ff92037a28
|
@@ -16,9 +16,6 @@ on:
|
|||||||
- '.ansible-lint'
|
- '.ansible-lint'
|
||||||
- '.gitea/workflows/ansible.yml'
|
- '.gitea/workflows/ansible.yml'
|
||||||
|
|
||||||
env:
|
|
||||||
ANSIBLE_COLLECTIONS_PATH: /root/.ansible/collections
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
runs-on: [self-hosted, pod]
|
runs-on: [self-hosted, pod]
|
||||||
@@ -30,6 +27,7 @@ jobs:
|
|||||||
python3 -m pip install --user --break-system-packages \
|
python3 -m pip install --user --break-system-packages \
|
||||||
--index-url https://pypi.org/simple --quiet uv==0.11.7
|
--index-url https://pypi.org/simple --quiet uv==0.11.7
|
||||||
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
||||||
|
echo "ANSIBLE_COLLECTIONS_PATH=$HOME/.ansible/collections" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
- name: Install ansible-lint and collections
|
- name: Install ansible-lint and collections
|
||||||
run: |
|
run: |
|
||||||
@@ -50,7 +48,7 @@ jobs:
|
|||||||
- name: ansible-lint
|
- name: ansible-lint
|
||||||
run: |
|
run: |
|
||||||
export PATH="$HOME/.local/bin:$PATH"
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
export ANSIBLE_COLLECTIONS_PATH="$PWD/infrastructure/samba-ad/ansible/collections:/root/.ansible/collections"
|
export ANSIBLE_COLLECTIONS_PATH="$PWD/infrastructure/samba-ad/ansible/collections:$HOME/.ansible/collections"
|
||||||
# 静态检查不应依赖生产 vault 凭据。一次性 checkout 可以去掉加密变量文件;
|
# 静态检查不应依赖生产 vault 凭据。一次性 checkout 可以去掉加密变量文件;
|
||||||
# syntax-check 只验证结构,不需要解析变量的运行时值。
|
# syntax-check 只验证结构,不需要解析变量的运行时值。
|
||||||
rm -f \
|
rm -f \
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
name: hydra-login
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'apps/hydra/login-consent/**'
|
||||||
|
- '.gitea/workflows/hydra.yml'
|
||||||
|
workflow_dispatch:
|
||||||
|
jobs:
|
||||||
|
verify:
|
||||||
|
runs-on: [self-hosted, pod]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version-file: apps/hydra/login-consent/go.mod
|
||||||
|
cache-dependency-path: apps/hydra/login-consent/go.sum
|
||||||
|
- name: Test authentication boundaries
|
||||||
|
working-directory: apps/hydra/login-consent
|
||||||
|
run: |
|
||||||
|
go test -race ./...
|
||||||
|
go vet ./...
|
||||||
|
CGO_ENABLED=0 go build -trimpath .
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Backstage
|
||||||
|
|
||||||
|
Backstage 作为 homelab 的只读开发者门户运行。应用源码、插件、测试和镜像构建归
|
||||||
|
`panxiao81/backstage` 仓库管理;本目录只保存 Kubernetes/Flux 部署声明,并通过
|
||||||
|
OCI digest 固定镜像。
|
||||||
|
|
||||||
|
## 外部前置
|
||||||
|
|
||||||
|
- OpenBao `kv/k8s/backstage` 必须包含以下与容器环境变量同名的字段:
|
||||||
|
`BACKEND_SECRET`、`AUTH_SESSION_SECRET`、`AUTH_OIDC_CLIENT_ID`、
|
||||||
|
`AUTH_OIDC_CLIENT_SECRET`、`POSTGRES_PASSWORD`、`GITEA_TOKEN`。
|
||||||
|
- PostgreSQL 需要在共享集群中预先创建由 `backstage` 角色拥有的 `backstage`
|
||||||
|
数据库;密码必须与 OpenBao 中的 `POSTGRES_PASSWORD` 一致。
|
||||||
|
- Authelia OIDC 客户端回调地址为
|
||||||
|
`https://backstage.ad.ddupan.top/api/auth/oidc/handler/frame`。
|
||||||
|
- AD DNS 需要将 `backstage.ad.ddupan.top` 指向 Envoy Gateway
|
||||||
|
`192.168.10.127`。
|
||||||
|
|
||||||
|
Flux 等待 ExternalSecret 和 Deployment 就绪;任何前置缺失都会使该
|
||||||
|
Kustomization 保持 NotReady,而不会回退到明文 Secret。
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: backstage
|
||||||
|
namespace: backstage
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: backstage
|
||||||
|
backstage.io/kubernetes-id: homelab-backstage
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
strategy:
|
||||||
|
type: Recreate
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/name: backstage
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: backstage
|
||||||
|
backstage.io/kubernetes-id: homelab-backstage
|
||||||
|
spec:
|
||||||
|
serviceAccountName: backstage
|
||||||
|
securityContext:
|
||||||
|
fsGroup: 1000
|
||||||
|
runAsNonRoot: true
|
||||||
|
seccompProfile:
|
||||||
|
type: RuntimeDefault
|
||||||
|
containers:
|
||||||
|
- name: backstage
|
||||||
|
image: zot.ad.ddupan.top/panxiao81/backstage@sha256:e5a12550726f19a680bc7c40e2cc07cc624318f9279ee121814d293a006ef210
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
env:
|
||||||
|
- name: BACKSTAGE_BASE_URL
|
||||||
|
value: https://backstage.ad.ddupan.top
|
||||||
|
- name: POSTGRES_HOST
|
||||||
|
value: shared-postgresql-rw.shared-db.svc.cluster.local
|
||||||
|
- name: POSTGRES_PORT
|
||||||
|
value: "5432"
|
||||||
|
- name: POSTGRES_USER
|
||||||
|
value: backstage
|
||||||
|
- name: POSTGRES_DATABASE
|
||||||
|
value: backstage
|
||||||
|
- name: GITEA_HOST
|
||||||
|
value: git.ddupan.top
|
||||||
|
envFrom:
|
||||||
|
- secretRef:
|
||||||
|
name: backstage
|
||||||
|
ports:
|
||||||
|
- containerPort: 7007
|
||||||
|
name: http
|
||||||
|
protocol: TCP
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /.backstage/health/v1/readiness
|
||||||
|
port: http
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 10
|
||||||
|
timeoutSeconds: 3
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /.backstage/health/v1/liveness
|
||||||
|
port: http
|
||||||
|
initialDelaySeconds: 30
|
||||||
|
periodSeconds: 20
|
||||||
|
timeoutSeconds: 3
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 512Mi
|
||||||
|
limits:
|
||||||
|
cpu: "1"
|
||||||
|
memory: 1Gi
|
||||||
|
securityContext:
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
capabilities:
|
||||||
|
drop: [ALL]
|
||||||
|
readOnlyRootFilesystem: true
|
||||||
|
runAsNonRoot: true
|
||||||
|
runAsUser: 1000
|
||||||
|
volumeMounts:
|
||||||
|
- mountPath: /tmp
|
||||||
|
name: tmp
|
||||||
|
volumes:
|
||||||
|
- emptyDir: {}
|
||||||
|
name: tmp
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
apiVersion: external-secrets.io/v1
|
||||||
|
kind: ExternalSecret
|
||||||
|
metadata:
|
||||||
|
name: backstage
|
||||||
|
namespace: backstage
|
||||||
|
spec:
|
||||||
|
refreshInterval: 1h
|
||||||
|
secretStoreRef:
|
||||||
|
kind: ClusterSecretStore
|
||||||
|
name: openbao
|
||||||
|
target:
|
||||||
|
creationPolicy: Owner
|
||||||
|
name: backstage
|
||||||
|
dataFrom:
|
||||||
|
- extract:
|
||||||
|
key: k8s/backstage
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
apiVersion: gateway.networking.k8s.io/v1
|
||||||
|
kind: HTTPRoute
|
||||||
|
metadata:
|
||||||
|
name: backstage
|
||||||
|
namespace: backstage
|
||||||
|
spec:
|
||||||
|
parentRefs:
|
||||||
|
- name: eg
|
||||||
|
namespace: envoy-gateway-system
|
||||||
|
sectionName: https
|
||||||
|
hostnames:
|
||||||
|
- backstage.ad.ddupan.top
|
||||||
|
rules:
|
||||||
|
- matches:
|
||||||
|
- path:
|
||||||
|
type: PathPrefix
|
||||||
|
value: /
|
||||||
|
backendRefs:
|
||||||
|
- name: backstage
|
||||||
|
port: 7007
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
resources:
|
||||||
|
- namespace.yaml
|
||||||
|
- serviceaccount.yaml
|
||||||
|
- rbac.yaml
|
||||||
|
- external-secret.yaml
|
||||||
|
- deployment.yaml
|
||||||
|
- service.yaml
|
||||||
|
- networkpolicy.yaml
|
||||||
|
- httproute.yaml
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: backstage
|
||||||
|
labels:
|
||||||
|
pod-security.kubernetes.io/enforce: restricted
|
||||||
|
pod-security.kubernetes.io/enforce-version: v1.36
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: NetworkPolicy
|
||||||
|
metadata:
|
||||||
|
name: backstage-ingress
|
||||||
|
namespace: backstage
|
||||||
|
spec:
|
||||||
|
podSelector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/name: backstage
|
||||||
|
policyTypes: [Ingress]
|
||||||
|
ingress:
|
||||||
|
- from:
|
||||||
|
- namespaceSelector:
|
||||||
|
matchLabels:
|
||||||
|
kubernetes.io/metadata.name: envoy-gateway-system
|
||||||
|
ports:
|
||||||
|
- port: 7007
|
||||||
|
protocol: TCP
|
||||||
|
- from:
|
||||||
|
- ipBlock:
|
||||||
|
cidr: 192.168.10.127/32
|
||||||
|
ports:
|
||||||
|
- port: 7007
|
||||||
|
protocol: TCP
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRole
|
||||||
|
metadata:
|
||||||
|
name: backstage-read-only
|
||||||
|
rules:
|
||||||
|
- apiGroups: [""]
|
||||||
|
resources:
|
||||||
|
- configmaps
|
||||||
|
- limitranges
|
||||||
|
- pods
|
||||||
|
- pods/log
|
||||||
|
- resourcequotas
|
||||||
|
- services
|
||||||
|
verbs: [get, list, watch]
|
||||||
|
- apiGroups: [apps]
|
||||||
|
resources:
|
||||||
|
- daemonsets
|
||||||
|
- deployments
|
||||||
|
- replicasets
|
||||||
|
- statefulsets
|
||||||
|
verbs: [get, list, watch]
|
||||||
|
- apiGroups: [autoscaling]
|
||||||
|
resources:
|
||||||
|
- horizontalpodautoscalers
|
||||||
|
verbs: [get, list, watch]
|
||||||
|
- apiGroups: [batch]
|
||||||
|
resources:
|
||||||
|
- cronjobs
|
||||||
|
- jobs
|
||||||
|
verbs: [get, list, watch]
|
||||||
|
- apiGroups: [networking.k8s.io]
|
||||||
|
resources:
|
||||||
|
- ingresses
|
||||||
|
verbs: [get, list, watch]
|
||||||
|
- apiGroups: [metrics.k8s.io]
|
||||||
|
resources:
|
||||||
|
- pods
|
||||||
|
verbs: [get, list]
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRoleBinding
|
||||||
|
metadata:
|
||||||
|
name: backstage-read-only
|
||||||
|
roleRef:
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
kind: ClusterRole
|
||||||
|
name: backstage-read-only
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: backstage
|
||||||
|
namespace: backstage
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: backstage
|
||||||
|
namespace: backstage
|
||||||
|
labels:
|
||||||
|
backstage.io/kubernetes-id: homelab-backstage
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app.kubernetes.io/name: backstage
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: 7007
|
||||||
|
protocol: TCP
|
||||||
|
targetPort: http
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
name: backstage
|
||||||
|
namespace: backstage
|
||||||
|
automountServiceAccountToken: true
|
||||||
@@ -44,3 +44,15 @@ API、OIDC、Git/Flux 和 runner 均已验证。第二跳按明确决定跳过
|
|||||||
结构,或者 release notes 指出相关 breaking migration 时,才恢复停机一致备份、分阶段
|
结构,或者 release notes 指出相关 breaking migration 时,才恢复停机一致备份、分阶段
|
||||||
suspend、详细日志审计和扩展验收。出现启动失败或 migration error 时也立即升级为完整
|
suspend、详细日志审计和扩展验收。出现启动失败或 migration error 时也立即升级为完整
|
||||||
故障流程。
|
故障流程。
|
||||||
|
|
||||||
|
## Hydra 人类登录 PoC
|
||||||
|
|
||||||
|
新增 `hydra` OIDC 登录源,旧 `authelia` 入口保留。Hydra 通过通用 OIDC Login/Consent
|
||||||
|
适配器转到现有 Authelia 完成人类认证;不是 Gitea 直接验证 LDAP 或 SPIFFE。
|
||||||
|
入口为 `https://git.ddupan.top/user/oauth2/hydra`,需 LAN/Tailscale 可达 Hydra 内网域名。
|
||||||
|
|
||||||
|
新 client secret 通过 `ExternalSecret/gitea-hydra-oidc` 从 OpenBao 投射。沿用
|
||||||
|
preferred_username、已验证邮箱与 groups;当前仍映射 gitea-admins,不在本轮切换组模型。
|
||||||
|
先部署并验证 Hydra discovery 后再接入本配置,避免 Gitea init 因上游不可达而失败。
|
||||||
|
|
||||||
|
实际登录验收与部署状态见 wiki;依赖和回退见 [Hydra README](../hydra/README.md)。
|
||||||
|
|||||||
@@ -116,6 +116,13 @@ gitea:
|
|||||||
scopes: openid profile email groups
|
scopes: openid profile email groups
|
||||||
groupClaimName: groups
|
groupClaimName: groups
|
||||||
adminGroup: gitea-admins
|
adminGroup: gitea-admins
|
||||||
|
- name: hydra
|
||||||
|
provider: openidConnect
|
||||||
|
existingSecret: gitea-hydra-oidc
|
||||||
|
autoDiscoverUrl: https://hydra.ad.ddupan.top/.well-known/openid-configuration
|
||||||
|
scopes: openid profile email groups
|
||||||
|
groupClaimName: groups
|
||||||
|
adminGroup: gitea-admins
|
||||||
|
|
||||||
persistence:
|
persistence:
|
||||||
size: 20Gi
|
size: 20Gi
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
apiVersion: external-secrets.io/v1
|
||||||
|
kind: ExternalSecret
|
||||||
|
metadata:
|
||||||
|
name: gitea-hydra-oidc
|
||||||
|
namespace: gitea
|
||||||
|
spec:
|
||||||
|
refreshInterval: 1h
|
||||||
|
secretStoreRef:
|
||||||
|
kind: ClusterSecretStore
|
||||||
|
name: openbao
|
||||||
|
target:
|
||||||
|
name: gitea-hydra-oidc
|
||||||
|
creationPolicy: Owner
|
||||||
|
template:
|
||||||
|
data:
|
||||||
|
key: gitea
|
||||||
|
secret: "{{ .client_secret }}"
|
||||||
|
data:
|
||||||
|
- secretKey: client_secret
|
||||||
|
remoteRef:
|
||||||
|
key: k8s/hydra
|
||||||
|
property: gitea_client_secret
|
||||||
@@ -16,3 +16,4 @@ resources:
|
|||||||
- helmrepository.yaml
|
- helmrepository.yaml
|
||||||
- helmrelease.yaml
|
- helmrelease.yaml
|
||||||
- httproute.yaml
|
- httproute.yaml
|
||||||
|
- hydra-external-secret.yaml
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
# Hydra 与 OIDC Login/Consent PoC
|
||||||
|
|
||||||
|
本目录提供独立 Hydra 签发服务,以及一个薄的 **OIDC 上游适配器**。当前上游配置为
|
||||||
|
Authelia;适配器不连接 LDAP,也不管理用户目录。Samba AD、密码和 MFA 继续由现有
|
||||||
|
Authelia 链路负责。第一轮只接入人类和 Gitea,不实现 agent 动态授权。
|
||||||
|
|
||||||
|
```text
|
||||||
|
Gitea → Hydra → OIDC Login/Consent → Authelia → Samba AD
|
||||||
|
← OIDC ← 经验证的上游身份 ← OIDC callback
|
||||||
|
```
|
||||||
|
|
||||||
|
目标入口:
|
||||||
|
|
||||||
|
- `https://hydra.ad.ddupan.top`:Hydra 公共 OAuth2/OIDC endpoint。
|
||||||
|
- `https://hydra-login.ad.ddupan.top`:上游 OIDC 登录及 consent 适配器。
|
||||||
|
- `hydra-admin.hydra.svc.cluster.local:4445`:仅集群内管理接口,无 HTTPRoute。
|
||||||
|
|
||||||
|
均为 LAN/Tailscale 入口,复用 Envoy `eg/https` wildcard TLS。没有增加公网 tunnel。
|
||||||
|
部署及实际验收状态以 wiki 和对应 PR 为准,文件存在不表示登录已验收。
|
||||||
|
|
||||||
|
## 首次使用与边界
|
||||||
|
|
||||||
|
在 Gitea 登录页选择 `hydra`,跳转到 Authelia 完成现有人类认证,再返回原有 Gitea
|
||||||
|
账号。旧 `authelia` 登录源保留。Gitea 的账号关联和资源权限仍由 Gitea 维护。
|
||||||
|
|
||||||
|
适配器要求验证上游 issuer、audience、签名、过期时间和 nonce,使用 PKCE S256,
|
||||||
|
并把单次 state 绑定到 Secure/HttpOnly/SameSite=Lax cookie。短期登录事务只存内存,
|
||||||
|
最多 1024 个、10 分钟过期;单副本重启后正在登录的用户需重试,不存人类密码或 token。
|
||||||
|
|
||||||
|
Hydra subject 为上游 `(issuer, sub)` 的 SHA-256 加 `human:` 前缀,与可变邮箱/用户名
|
||||||
|
分离。第一轮要求上游返回经过验证的 email 及 preferred_username;这些 claims 必须
|
||||||
|
明确配置进 ID token。更换 issuer 会改变本 PoC 的 subject,正式迁移前需要身份绑定设计。
|
||||||
|
|
||||||
|
仅为显式 `ALLOWED_CLIENTS=gitea` 自动 consent,scope 限于 openid/profile/email/groups;
|
||||||
|
拒绝额外 access-token audience,不发 refresh token。只按实际请求 scope 释放 claims。
|
||||||
|
这不是通用的无人确认授权服务。组当前透传,沿用 Gitea 的 gitea-admins 映射;统一组
|
||||||
|
模型和 agent 认证均在后续阶段。不存在对 Authelia 专有协议的调用。
|
||||||
|
|
||||||
|
NetworkPolicy 限制公共端口只接收 Envoy 流量,Hydra admin 只允许适配器访问。
|
||||||
|
Hydra 使用正式模式,TLS 由 Envoy 终止;不使用 `--dev`。管理操作使用受控
|
||||||
|
`kubectl port-forward`,不要将 admin 接口暴露到 Gateway。
|
||||||
|
|
||||||
|
## 依赖、秘密与初始化
|
||||||
|
|
||||||
|
依赖共享 CloudNativePG、OpenBao/ESO、Authelia OIDC、Envoy、Samba DNS、zot 镜像仓库。
|
||||||
|
Hydra 使用独立 `hydra` database/role,不与其他应用共享数据库角色。
|
||||||
|
|
||||||
|
`kv/k8s/hydra` 保存 dsn、system_secret、upstream_client_secret、upstream_client_digest、
|
||||||
|
gitea_client_secret;通过 ExternalSecret 投射,值不写入 Git。Bootstrap 创建角色及数据库
|
||||||
|
后才启动 Hydra migration。system_secret 必须持久保存,不得在重启时随机重建。
|
||||||
|
|
||||||
|
Authelia 中新增 confidential client `hydra-login`:
|
||||||
|
|
||||||
|
- redirect URI:`https://hydra-login.ad.ddupan.top/callback`;
|
||||||
|
- authorization policy:two_factor;grant:authorization_code;PKCE:S256;
|
||||||
|
- token endpoint auth:client_secret_basic;scope:openid/profile/email/groups;
|
||||||
|
- claims policy:把 preferred_username、name、email、email_verified、groups 放入 ID token;
|
||||||
|
- client secret 的 PBKDF2 digest 存入 Authelia,原值仅供适配器使用。
|
||||||
|
|
||||||
|
Authelia 尚非 Flux 管理。修改 Helm values 时保留所有已有 clients 与 secret 引用,
|
||||||
|
通过 `--reuse-values` 和最小 overlay 增加客户端,不能以本目录配置覆盖其完整 values。
|
||||||
|
|
||||||
|
Hydra 中注册 confidential client `gitea`,redirect URI 为
|
||||||
|
`https://git.ddupan.top/user/oauth2/hydra/callback`,grant/response 为 authorization_code/code,
|
||||||
|
scope 为 openid/profile/email/groups,token endpoint auth 为 client_secret_basic。
|
||||||
|
Gitea 启动时读取 OIDC discovery,所以应先确认 Hydra 健康和 discovery 可达,再接入 Gitea。
|
||||||
|
|
||||||
|
## 构建与检查
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd apps/hydra/login-consent
|
||||||
|
go test -race ./...
|
||||||
|
go vet ./...
|
||||||
|
CGO_ENABLED=0 go build -trimpath -ldflags='-s -w' -o login-consent .
|
||||||
|
docker build -t hydra-login-consent:VERSION .
|
||||||
|
```
|
||||||
|
|
||||||
|
Go module 独立,依赖由 go.sum 锁定;Dockerfile 固定基础镜像 digest。
|
||||||
|
使用已授权的短期 SPIFFE zot 凭据发布镜像,部署使用匿名拉取入口与不可变 digest。
|
||||||
|
不把 registry 凭据写入源码或 build args。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl kustomize apps/hydra
|
||||||
|
sudo k3s kubectl -n hydra get deployment,pod,externalsecret,httproute
|
||||||
|
sudo k3s kubectl -n hydra logs deployment/hydra -c migrate
|
||||||
|
sudo k3s kubectl -n hydra logs deployment/hydra-login
|
||||||
|
```
|
||||||
|
|
||||||
|
日志不输出上游 token、授权 code、challenge 或秘密。登录失败先查两端 Pod 状态、
|
||||||
|
DNS/discovery 连通性、client redirect URI 和 scope,再由用户重新发起登录。
|
||||||
|
不要在故障排查中关闭签名验证、MFA 或 state/nonce 校验。
|
||||||
|
|
||||||
|
## 恢复与回退
|
||||||
|
|
||||||
|
保留共享 PostgreSQL 中 Hydra 数据及 OpenBao 秘密;数据库持有 clients、会话及签名密钥,
|
||||||
|
单独重建 Deployment 不能替代恢复数据库。先恢复依赖,再启动 Hydra 和适配器。
|
||||||
|
当前恢复仍依赖 homelab 共享基础设施,不能声称已完成独立灾备。
|
||||||
|
|
||||||
|
第一轮不切换 Authelia 的主入口。撤回 Gitea 的新增 Hydra 登录源即可回到旧入口;
|
||||||
|
先撤消费者,再考虑停用 Hydra。不要删除旧 Authelia 登录源、用户或数据库作为回退手段。
|
||||||
|
|
||||||
|
跨服务设计见 [独立 IAM 草案](https://git.ddupan.top/panxiao81/homelab-wiki/src/branch/main/architecture/independent-iam-draft.md)。
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: hydra
|
||||||
|
namespace: hydra
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: hydra
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: hydra
|
||||||
|
spec:
|
||||||
|
automountServiceAccountToken: false
|
||||||
|
securityContext:
|
||||||
|
runAsNonRoot: true
|
||||||
|
runAsUser: 1000
|
||||||
|
runAsGroup: 1000
|
||||||
|
seccompProfile:
|
||||||
|
type: RuntimeDefault
|
||||||
|
initContainers:
|
||||||
|
- name: migrate
|
||||||
|
image: docker.io/oryd/hydra:v26.2.0@sha256:ff67c7fb5f95074fa53374d41151713554960504b340cd3f95b09e65deaea2a9
|
||||||
|
args:
|
||||||
|
- migrate
|
||||||
|
- sql
|
||||||
|
- -e
|
||||||
|
- --yes
|
||||||
|
env:
|
||||||
|
- name: DSN
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: hydra
|
||||||
|
key: dsn
|
||||||
|
securityContext: &id002
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
readOnlyRootFilesystem: true
|
||||||
|
capabilities:
|
||||||
|
drop:
|
||||||
|
- ALL
|
||||||
|
resources: &id001
|
||||||
|
requests:
|
||||||
|
cpu: 50m
|
||||||
|
memory: 64Mi
|
||||||
|
limits:
|
||||||
|
memory: 256Mi
|
||||||
|
containers:
|
||||||
|
- name: hydra
|
||||||
|
image: docker.io/oryd/hydra:v26.2.0@sha256:ff67c7fb5f95074fa53374d41151713554960504b340cd3f95b09e65deaea2a9
|
||||||
|
args:
|
||||||
|
- serve
|
||||||
|
- all
|
||||||
|
- --config
|
||||||
|
- /etc/hydra/hydra.yaml
|
||||||
|
- --sqa-opt-out
|
||||||
|
env:
|
||||||
|
- name: DSN
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: hydra
|
||||||
|
key: dsn
|
||||||
|
- name: SECRETS_SYSTEM
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: hydra
|
||||||
|
key: system_secret
|
||||||
|
ports:
|
||||||
|
- name: public
|
||||||
|
containerPort: 4444
|
||||||
|
- name: admin
|
||||||
|
containerPort: 4445
|
||||||
|
resources: *id001
|
||||||
|
securityContext: *id002
|
||||||
|
volumeMounts:
|
||||||
|
- name: config
|
||||||
|
mountPath: /etc/hydra
|
||||||
|
readOnly: true
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health/ready
|
||||||
|
port: admin
|
||||||
|
initialDelaySeconds: 5
|
||||||
|
periodSeconds: 5
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health/alive
|
||||||
|
port: admin
|
||||||
|
initialDelaySeconds: 20
|
||||||
|
periodSeconds: 20
|
||||||
|
volumes:
|
||||||
|
- name: config
|
||||||
|
configMap:
|
||||||
|
name: hydra-config
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
apiVersion: external-secrets.io/v1
|
||||||
|
kind: ExternalSecret
|
||||||
|
metadata:
|
||||||
|
name: hydra
|
||||||
|
namespace: hydra
|
||||||
|
spec:
|
||||||
|
refreshInterval: 1h
|
||||||
|
secretStoreRef:
|
||||||
|
kind: ClusterSecretStore
|
||||||
|
name: openbao
|
||||||
|
target:
|
||||||
|
name: hydra
|
||||||
|
creationPolicy: Owner
|
||||||
|
dataFrom:
|
||||||
|
- extract:
|
||||||
|
key: k8s/hydra
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
apiVersion: gateway.networking.k8s.io/v1
|
||||||
|
kind: HTTPRoute
|
||||||
|
metadata:
|
||||||
|
name: hydra-public
|
||||||
|
namespace: hydra
|
||||||
|
spec:
|
||||||
|
parentRefs:
|
||||||
|
- name: eg
|
||||||
|
namespace: envoy-gateway-system
|
||||||
|
sectionName: https
|
||||||
|
hostnames:
|
||||||
|
- hydra.ad.ddupan.top
|
||||||
|
rules:
|
||||||
|
- backendRefs:
|
||||||
|
- name: hydra-public
|
||||||
|
port: 4444
|
||||||
|
---
|
||||||
|
apiVersion: gateway.networking.k8s.io/v1
|
||||||
|
kind: HTTPRoute
|
||||||
|
metadata:
|
||||||
|
name: hydra-login
|
||||||
|
namespace: hydra
|
||||||
|
spec:
|
||||||
|
parentRefs:
|
||||||
|
- name: eg
|
||||||
|
namespace: envoy-gateway-system
|
||||||
|
sectionName: https
|
||||||
|
hostnames:
|
||||||
|
- hydra-login.ad.ddupan.top
|
||||||
|
rules:
|
||||||
|
- backendRefs:
|
||||||
|
- name: hydra-login
|
||||||
|
port: 8080
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
serve:
|
||||||
|
public:
|
||||||
|
port: 4444
|
||||||
|
admin:
|
||||||
|
port: 4445
|
||||||
|
tls:
|
||||||
|
allow_termination_from:
|
||||||
|
- 10.42.0.0/16
|
||||||
|
cookies:
|
||||||
|
same_site_mode: Lax
|
||||||
|
urls:
|
||||||
|
self:
|
||||||
|
issuer: https://hydra.ad.ddupan.top
|
||||||
|
public: https://hydra.ad.ddupan.top
|
||||||
|
login: https://hydra-login.ad.ddupan.top/login
|
||||||
|
consent: https://hydra-login.ad.ddupan.top/consent
|
||||||
|
ttl:
|
||||||
|
access_token: 15m
|
||||||
|
id_token: 15m
|
||||||
|
auth_code: 5m
|
||||||
|
log:
|
||||||
|
level: info
|
||||||
|
leak_sensitive_values: false
|
||||||
|
oauth2:
|
||||||
|
expose_internal_errors: false
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
resources:
|
||||||
|
- namespace.yaml
|
||||||
|
- external-secret.yaml
|
||||||
|
- deployment.yaml
|
||||||
|
- login-deployment.yaml
|
||||||
|
- services.yaml
|
||||||
|
- httproutes.yaml
|
||||||
|
- networkpolicy.yaml
|
||||||
|
configMapGenerator:
|
||||||
|
- name: hydra-config
|
||||||
|
namespace: hydra
|
||||||
|
files:
|
||||||
|
- hydra.yaml
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/login-consent
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
FROM gcr.io/distroless/static-debian12:nonroot@sha256:afa5c872c891853ca7fcf1f12c3edb23f7eeef36189728842dd51042ff57f7ab
|
||||||
|
COPY login-consent /login-consent
|
||||||
|
USER 65532:65532
|
||||||
|
ENTRYPOINT ["/login-consent"]
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
module git.ddupan.top/panxiao81/homelab-infra/apps/hydra/login-consent
|
||||||
|
|
||||||
|
go 1.26.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/coreos/go-oidc/v3 v3.14.1
|
||||||
|
golang.org/x/oauth2 v0.37.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
|
||||||
|
golang.org/x/crypto v0.36.0 // indirect
|
||||||
|
)
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk=
|
||||||
|
github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
|
||||||
|
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
|
||||||
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||||
|
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||||
|
golang.org/x/oauth2 v0.37.0 h1:JUlcxA8oAtauLfiH8FX2/FkAWHAdi0QtGCGc+hofE98=
|
||||||
|
golang.org/x/oauth2 v0.37.0/go.mod h1:IxwZNxUULJmpBFf9K/9NTMSIfZZuvuTy1gGxhigP/58=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
@@ -0,0 +1,285 @@
|
|||||||
|
// Login/Consent adapter for a single trusted upstream and first-party clients.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/subtle"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/coreos/go-oidc/v3/oidc"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
)
|
||||||
|
|
||||||
|
const cookieName = "__Host-hydra-login"
|
||||||
|
|
||||||
|
type pending struct {
|
||||||
|
Challenge, Nonce, Verifier string
|
||||||
|
Expires time.Time
|
||||||
|
}
|
||||||
|
type claims struct {
|
||||||
|
Username string `json:"preferred_username"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
EmailVerified bool `json:"email_verified"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Groups []string `json:"groups"`
|
||||||
|
}
|
||||||
|
type flowRequest struct {
|
||||||
|
Client struct {
|
||||||
|
ID string `json:"client_id"`
|
||||||
|
} `json:"client"`
|
||||||
|
Subject string `json:"subject"`
|
||||||
|
Scopes []string `json:"requested_scope"`
|
||||||
|
Audience []string `json:"requested_access_token_audience"`
|
||||||
|
Context claims `json:"context"`
|
||||||
|
}
|
||||||
|
type app struct {
|
||||||
|
admin, public string
|
||||||
|
client *http.Client
|
||||||
|
oauth oauth2.Config
|
||||||
|
verifier *oidc.IDTokenVerifier
|
||||||
|
allowed map[string]bool
|
||||||
|
mu sync.Mutex
|
||||||
|
pending map[string]pending
|
||||||
|
}
|
||||||
|
|
||||||
|
func required(key string) string {
|
||||||
|
v := os.Getenv(key)
|
||||||
|
if v == "" {
|
||||||
|
log.Fatalf("missing %s", key)
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
func random() string {
|
||||||
|
b := make([]byte, 32)
|
||||||
|
if _, err := rand.Read(b); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return base64.RawURLEncoding.EncodeToString(b)
|
||||||
|
}
|
||||||
|
func (a *app) api(ctx context.Context, method, path string, in, out any) error {
|
||||||
|
var body io.Reader
|
||||||
|
if in != nil {
|
||||||
|
b, err := json.Marshal(in)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
body = bytes.NewReader(b)
|
||||||
|
}
|
||||||
|
req, err := http.NewRequestWithContext(ctx, method, a.admin+path, body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
resp, err := a.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("Hydra unavailable")
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
|
return fmt.Errorf("Hydra status %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
if out != nil {
|
||||||
|
return json.NewDecoder(io.LimitReader(resp.Body, 1<<20)).Decode(out)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (a *app) request(r *http.Request, kind, challenge string) (flowRequest, error) {
|
||||||
|
var f flowRequest
|
||||||
|
if challenge == "" || len(challenge) > 8192 {
|
||||||
|
return f, errors.New("missing or invalid challenge")
|
||||||
|
}
|
||||||
|
err := a.api(r.Context(), http.MethodGet, "/admin/oauth2/auth/requests/"+kind+"?"+kind+"_challenge="+url.QueryEscape(challenge), nil, &f)
|
||||||
|
if err != nil {
|
||||||
|
return f, err
|
||||||
|
}
|
||||||
|
if !a.allowed[f.Client.ID] {
|
||||||
|
return f, errors.New("client not allowed")
|
||||||
|
}
|
||||||
|
return f, nil
|
||||||
|
}
|
||||||
|
func (a *app) accept(w http.ResponseWriter, r *http.Request, kind, challenge string, body any) {
|
||||||
|
var result struct {
|
||||||
|
Redirect string `json:"redirect_to"`
|
||||||
|
}
|
||||||
|
if err := a.api(r.Context(), http.MethodPut, "/admin/oauth2/auth/requests/"+kind+"/accept?"+kind+"_challenge="+url.QueryEscape(challenge), body, &result); err != nil {
|
||||||
|
fail(w, 502)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Only Hydra's own authorization endpoint can receive a challenge verifier.
|
||||||
|
u, err := url.Parse(result.Redirect)
|
||||||
|
p, _ := url.Parse(a.public)
|
||||||
|
if err != nil || u.Scheme != p.Scheme || u.Host != p.Host || u.User != nil || u.Path != "/oauth2/auth" {
|
||||||
|
fail(w, 502)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.Redirect(w, r, result.Redirect, http.StatusSeeOther)
|
||||||
|
}
|
||||||
|
func fail(w http.ResponseWriter, status int) { http.Error(w, http.StatusText(status), status) }
|
||||||
|
func (a *app) login(w http.ResponseWriter, r *http.Request) {
|
||||||
|
challenge := r.URL.Query().Get("login_challenge")
|
||||||
|
if _, err := a.request(r, "login", challenge); err != nil {
|
||||||
|
fail(w, 403)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
state := random()
|
||||||
|
p := pending{challenge, random(), oauth2.GenerateVerifier(), time.Now().Add(10 * time.Minute)}
|
||||||
|
a.mu.Lock()
|
||||||
|
for k, v := range a.pending {
|
||||||
|
if time.Now().After(v.Expires) {
|
||||||
|
delete(a.pending, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(a.pending) >= 1024 {
|
||||||
|
a.mu.Unlock()
|
||||||
|
fail(w, 503)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
a.pending[state] = p
|
||||||
|
a.mu.Unlock()
|
||||||
|
http.SetCookie(w, &http.Cookie{Name: cookieName, Value: state, Path: "/", Secure: true, HttpOnly: true, SameSite: http.SameSiteLaxMode, MaxAge: 600})
|
||||||
|
http.Redirect(w, r, a.oauth.AuthCodeURL(state, oidc.Nonce(p.Nonce), oauth2.S256ChallengeOption(p.Verifier)), http.StatusSeeOther)
|
||||||
|
}
|
||||||
|
func (a *app) take(r *http.Request) (pending, error) {
|
||||||
|
state := r.URL.Query().Get("state")
|
||||||
|
cookie, err := r.Cookie(cookieName)
|
||||||
|
if err != nil || state == "" || subtle.ConstantTimeCompare([]byte(cookie.Value), []byte(state)) != 1 {
|
||||||
|
return pending{}, errors.New("state mismatch")
|
||||||
|
}
|
||||||
|
a.mu.Lock()
|
||||||
|
defer a.mu.Unlock()
|
||||||
|
p, ok := a.pending[state]
|
||||||
|
delete(a.pending, state)
|
||||||
|
if !ok || time.Now().After(p.Expires) {
|
||||||
|
return pending{}, errors.New("expired or used state")
|
||||||
|
}
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
func (a *app) callback(w http.ResponseWriter, r *http.Request) {
|
||||||
|
p, err := a.take(r)
|
||||||
|
if err != nil {
|
||||||
|
fail(w, 403)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.SetCookie(w, &http.Cookie{Name: cookieName, Path: "/", Secure: true, HttpOnly: true, SameSite: http.SameSiteLaxMode, MaxAge: -1})
|
||||||
|
if r.URL.Query().Get("error") != "" || r.URL.Query().Get("code") == "" {
|
||||||
|
fail(w, 403)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx := oidc.ClientContext(r.Context(), a.client)
|
||||||
|
token, err := a.oauth.Exchange(ctx, r.URL.Query().Get("code"), oauth2.VerifierOption(p.Verifier))
|
||||||
|
if err != nil {
|
||||||
|
fail(w, 502)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
raw, ok := token.Extra("id_token").(string)
|
||||||
|
if !ok {
|
||||||
|
fail(w, 502)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id, err := a.verifier.Verify(ctx, raw)
|
||||||
|
if err != nil || id.Nonce != p.Nonce || id.Subject == "" {
|
||||||
|
fail(w, 403)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var c claims
|
||||||
|
if id.Claims(&c) != nil || c.Username == "" || c.Email == "" || !c.EmailVerified {
|
||||||
|
fail(w, 403)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err := a.request(r, "login", p.Challenge); err != nil {
|
||||||
|
fail(w, 403)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Stable identity is tied to the verified upstream issuer+subject, never email.
|
||||||
|
sum := sha256.Sum256([]byte(id.Issuer + "\x00" + id.Subject))
|
||||||
|
a.accept(w, r, "login", p.Challenge, map[string]any{"subject": "human:" + hex.EncodeToString(sum[:]), "remember": false, "context": c})
|
||||||
|
}
|
||||||
|
func consentSession(f flowRequest) (map[string]any, error) {
|
||||||
|
if !strings.HasPrefix(f.Subject, "human:") || f.Context.Username == "" || f.Context.Email == "" || !f.Context.EmailVerified {
|
||||||
|
return nil, errors.New("invalid identity context")
|
||||||
|
}
|
||||||
|
allowed := map[string]bool{"openid": true, "profile": true, "email": true, "groups": true}
|
||||||
|
session := map[string]any{"principal_type": "human"}
|
||||||
|
for _, scope := range f.Scopes {
|
||||||
|
if !allowed[scope] {
|
||||||
|
return nil, errors.New("scope not allowed")
|
||||||
|
}
|
||||||
|
switch scope {
|
||||||
|
case "profile":
|
||||||
|
session["preferred_username"] = f.Context.Username
|
||||||
|
session["name"] = f.Context.Name
|
||||||
|
case "email":
|
||||||
|
session["email"] = f.Context.Email
|
||||||
|
session["email_verified"] = true
|
||||||
|
case "groups":
|
||||||
|
session["groups"] = f.Context.Groups
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(f.Audience) > 0 {
|
||||||
|
return nil, errors.New("access token audience not allowed")
|
||||||
|
}
|
||||||
|
return session, nil
|
||||||
|
}
|
||||||
|
func (a *app) consent(w http.ResponseWriter, r *http.Request) {
|
||||||
|
challenge := r.URL.Query().Get("consent_challenge")
|
||||||
|
f, err := a.request(r, "consent", challenge)
|
||||||
|
if err != nil {
|
||||||
|
fail(w, 403)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
session, err := consentSession(f)
|
||||||
|
if err != nil {
|
||||||
|
fail(w, 403)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Explicit policy for pre-approved first-party clients only; no generic auto-consent.
|
||||||
|
a.accept(w, r, "consent", challenge, map[string]any{"grant_scope": f.Scopes, "remember": false, "session": map[string]any{"id_token": session}})
|
||||||
|
}
|
||||||
|
func (a *app) handler() http.Handler {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) })
|
||||||
|
mux.HandleFunc("GET /login", a.login)
|
||||||
|
mux.HandleFunc("GET /callback", a.callback)
|
||||||
|
mux.HandleFunc("GET /consent", a.consent)
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Cache-Control", "no-store")
|
||||||
|
w.Header().Set("Referrer-Policy", "no-referrer")
|
||||||
|
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||||
|
w.Header().Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'")
|
||||||
|
mux.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
func main() {
|
||||||
|
client := &http.Client{Timeout: 15 * time.Second, CheckRedirect: func(r *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }}
|
||||||
|
issuer := required("UPSTREAM_ISSUER")
|
||||||
|
ctx := oidc.ClientContext(context.Background(), client)
|
||||||
|
provider, err := oidc.NewProvider(ctx, issuer)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("upstream discovery failed")
|
||||||
|
}
|
||||||
|
clientID := required("UPSTREAM_CLIENT_ID")
|
||||||
|
a := &app{admin: required("HYDRA_ADMIN_URL"), public: required("HYDRA_PUBLIC_URL"), client: client, allowed: map[string]bool{}, pending: map[string]pending{},
|
||||||
|
oauth: oauth2.Config{ClientID: clientID, ClientSecret: required("UPSTREAM_CLIENT_SECRET"), RedirectURL: required("CALLBACK_URL"), Endpoint: provider.Endpoint(), Scopes: []string{"openid", "profile", "email", "groups"}},
|
||||||
|
verifier: provider.Verifier(&oidc.Config{ClientID: clientID})}
|
||||||
|
for _, id := range strings.Split(required("ALLOWED_CLIENTS"), ",") {
|
||||||
|
a.allowed[id] = true
|
||||||
|
}
|
||||||
|
s := http.Server{Addr: ":8080", Handler: a.handler(), ReadHeaderTimeout: 5 * time.Second, ReadTimeout: 20 * time.Second, WriteTimeout: 45 * time.Second, IdleTimeout: 60 * time.Second, MaxHeaderBytes: 16384}
|
||||||
|
log.Print("login/consent adapter listening on :8080")
|
||||||
|
log.Fatal(s.ListenAndServe())
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStateBoundToCookieSingleUseAndExpiry(t *testing.T) {
|
||||||
|
a := &app{pending: map[string]pending{"valid": {Challenge: "challenge", Expires: time.Now().Add(time.Minute)}, "expired": {Expires: time.Now().Add(-time.Minute)}}}
|
||||||
|
request := func(state, cookie string) *http.Request {
|
||||||
|
r := httptest.NewRequest("GET", "https://login.example/callback?state="+state, nil)
|
||||||
|
if cookie != "" {
|
||||||
|
r.AddCookie(&http.Cookie{Name: cookieName, Value: cookie})
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
for _, r := range []*http.Request{request("valid", ""), request("valid", "other"), request("expired", "expired")} {
|
||||||
|
if _, err := a.take(r); err == nil {
|
||||||
|
t.Fatal("invalid state accepted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if p, err := a.take(request("valid", "valid")); err != nil || p.Challenge != "challenge" {
|
||||||
|
t.Fatal("valid state rejected")
|
||||||
|
}
|
||||||
|
if _, err := a.take(request("valid", "valid")); err == nil {
|
||||||
|
t.Fatal("replayed state accepted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func TestConsentRejectsPrivilegeExpansionAndFiltersClaims(t *testing.T) {
|
||||||
|
f := flowRequest{Subject: "human:known", Scopes: []string{"openid", "email"}, Context: claims{Username: "alice", Email: "[email protected]", EmailVerified: true, Groups: []string{"operators"}}}
|
||||||
|
s, err := consentSession(f)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, ok := s["groups"]; ok {
|
||||||
|
t.Fatal("groups leaked without scope")
|
||||||
|
}
|
||||||
|
if _, ok := s["preferred_username"]; ok {
|
||||||
|
t.Fatal("profile leaked without scope")
|
||||||
|
}
|
||||||
|
for _, scope := range []string{"admin", "offline_access", "unknown"} {
|
||||||
|
bad := f
|
||||||
|
bad.Scopes = append([]string{"openid"}, scope)
|
||||||
|
if _, err := consentSession(bad); err == nil {
|
||||||
|
t.Fatalf("accepted %s", scope)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f.Audience = []string{"other-service"}
|
||||||
|
if _, err := consentSession(f); err == nil {
|
||||||
|
t.Fatal("unexpected audience accepted")
|
||||||
|
}
|
||||||
|
f.Audience = nil
|
||||||
|
f.Context.EmailVerified = false
|
||||||
|
if _, err := consentSession(f); err == nil {
|
||||||
|
t.Fatal("unverified email accepted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func TestLoginValidatesClientAndUsesPKCEAndNonce(t *testing.T) {
|
||||||
|
admin := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
json.NewEncoder(w).Encode(map[string]any{"client": map[string]string{"client_id": r.URL.Query().Get("login_challenge")}})
|
||||||
|
}))
|
||||||
|
defer admin.Close()
|
||||||
|
a := &app{admin: admin.URL, client: admin.Client(), allowed: map[string]bool{"gitea": true}, pending: map[string]pending{}, oauth: oauth2.Config{ClientID: "hydra-login", RedirectURL: "https://login.example/callback", Endpoint: oauth2.Endpoint{AuthURL: "https://upstream.example/authorize"}}}
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
a.handler().ServeHTTP(w, httptest.NewRequest("GET", "https://login.example/login?login_challenge=rogue", nil))
|
||||||
|
if w.Code != 403 {
|
||||||
|
t.Fatal("unknown client accepted")
|
||||||
|
}
|
||||||
|
w = httptest.NewRecorder()
|
||||||
|
a.handler().ServeHTTP(w, httptest.NewRequest("GET", "https://login.example/login?login_challenge=gitea", nil))
|
||||||
|
if w.Code != 303 {
|
||||||
|
t.Fatalf("status %d", w.Code)
|
||||||
|
}
|
||||||
|
u, _ := url.Parse(w.Header().Get("Location"))
|
||||||
|
q := u.Query()
|
||||||
|
if q.Get("code_challenge_method") != "S256" || q.Get("code_challenge") == "" || q.Get("nonce") == "" || q.Get("state") == "" {
|
||||||
|
t.Fatal("missing protocol binding")
|
||||||
|
}
|
||||||
|
cookies := w.Result().Cookies()
|
||||||
|
if len(cookies) != 1 || !cookies[0].Secure || !cookies[0].HttpOnly || cookies[0].SameSite != http.SameSiteLaxMode || cookies[0].Value != q.Get("state") {
|
||||||
|
t.Fatal("unsafe cookie")
|
||||||
|
}
|
||||||
|
if w.Header().Get("Cache-Control") != "no-store" {
|
||||||
|
t.Fatal("missing cache protection")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func TestHydraRedirectCannotLeaveTrustedOrigin(t *testing.T) {
|
||||||
|
for _, target := range []string{"https://evil.example/oauth2/auth", "https://[email protected]/oauth2/auth", "https://hydra.example/other"} {
|
||||||
|
admin := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{"redirect_to": target})
|
||||||
|
}))
|
||||||
|
a := &app{admin: admin.URL, public: "https://hydra.example", client: admin.Client()}
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
a.accept(w, httptest.NewRequest("GET", "https://login.example/login", nil), "login", "challenge", map[string]string{"subject": "human:test"})
|
||||||
|
if w.Code != 502 || strings.Contains(w.Header().Get("Location"), "evil") {
|
||||||
|
t.Fatal("untrusted redirect accepted")
|
||||||
|
}
|
||||||
|
admin.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: hydra-login
|
||||||
|
namespace: hydra
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
strategy:
|
||||||
|
type: Recreate
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: hydra-login
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: hydra-login
|
||||||
|
spec:
|
||||||
|
automountServiceAccountToken: false
|
||||||
|
securityContext:
|
||||||
|
runAsNonRoot: true
|
||||||
|
runAsUser: 65532
|
||||||
|
runAsGroup: 65532
|
||||||
|
seccompProfile:
|
||||||
|
type: RuntimeDefault
|
||||||
|
containers:
|
||||||
|
- name: login-consent
|
||||||
|
image: zot.ad.ddupan.top/iam/oidc-login-consent@sha256:fede9b9e93c457c4b7a8a6022d9df86ff5d5900d3f6b6c4f3439851a0ae1e944
|
||||||
|
env:
|
||||||
|
- name: HYDRA_ADMIN_URL
|
||||||
|
value: http://hydra-admin.hydra.svc.cluster.local:4445
|
||||||
|
- name: HYDRA_PUBLIC_URL
|
||||||
|
value: https://hydra.ad.ddupan.top
|
||||||
|
- name: UPSTREAM_ISSUER
|
||||||
|
value: https://auth.ddupan.top
|
||||||
|
- name: UPSTREAM_CLIENT_ID
|
||||||
|
value: hydra-login
|
||||||
|
- name: CALLBACK_URL
|
||||||
|
value: https://hydra-login.ad.ddupan.top/callback
|
||||||
|
- name: ALLOWED_CLIENTS
|
||||||
|
value: gitea
|
||||||
|
- name: UPSTREAM_CLIENT_SECRET
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: hydra
|
||||||
|
key: upstream_client_secret
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
containerPort: 8080
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 50m
|
||||||
|
memory: 64Mi
|
||||||
|
limits:
|
||||||
|
memory: 256Mi
|
||||||
|
securityContext:
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
readOnlyRootFilesystem: true
|
||||||
|
capabilities:
|
||||||
|
drop:
|
||||||
|
- ALL
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /healthz
|
||||||
|
port: http
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /healthz
|
||||||
|
port: http
|
||||||
|
initialDelaySeconds: 10
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: hydra
|
||||||
|
labels:
|
||||||
|
pod-security.kubernetes.io/enforce: restricted
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: NetworkPolicy
|
||||||
|
metadata:
|
||||||
|
name: hydra
|
||||||
|
namespace: hydra
|
||||||
|
spec:
|
||||||
|
podSelector:
|
||||||
|
matchLabels:
|
||||||
|
app: hydra
|
||||||
|
policyTypes:
|
||||||
|
- Ingress
|
||||||
|
ingress:
|
||||||
|
- from:
|
||||||
|
- namespaceSelector:
|
||||||
|
matchLabels:
|
||||||
|
kubernetes.io/metadata.name: envoy-gateway-system
|
||||||
|
ports:
|
||||||
|
- port: 4444
|
||||||
|
protocol: TCP
|
||||||
|
- from:
|
||||||
|
- podSelector:
|
||||||
|
matchLabels:
|
||||||
|
app: hydra-login
|
||||||
|
ports:
|
||||||
|
- port: 4445
|
||||||
|
protocol: TCP
|
||||||
|
---
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: NetworkPolicy
|
||||||
|
metadata:
|
||||||
|
name: hydra-login
|
||||||
|
namespace: hydra
|
||||||
|
spec:
|
||||||
|
podSelector:
|
||||||
|
matchLabels:
|
||||||
|
app: hydra-login
|
||||||
|
policyTypes:
|
||||||
|
- Ingress
|
||||||
|
ingress:
|
||||||
|
- from:
|
||||||
|
- namespaceSelector:
|
||||||
|
matchLabels:
|
||||||
|
kubernetes.io/metadata.name: envoy-gateway-system
|
||||||
|
ports:
|
||||||
|
- port: 8080
|
||||||
|
protocol: TCP
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: hydra-public
|
||||||
|
namespace: hydra
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: hydra
|
||||||
|
ports:
|
||||||
|
- port: 4444
|
||||||
|
targetPort: 4444
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: hydra-admin
|
||||||
|
namespace: hydra
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: hydra
|
||||||
|
ports:
|
||||||
|
- port: 4445
|
||||||
|
targetPort: 4445
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: hydra-login
|
||||||
|
namespace: hydra
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: hydra-login
|
||||||
|
ports:
|
||||||
|
- port: 8080
|
||||||
|
targetPort: 8080
|
||||||
+29
-5
@@ -70,11 +70,11 @@ configFiles:
|
|||||||
},
|
},
|
||||||
"accessControl": {
|
"accessControl": {
|
||||||
"repositories": {
|
"repositories": {
|
||||||
"panxiao81/gitea-dynamic-runner-controller": {
|
"panxiao81/backstage": {
|
||||||
"policies": [
|
"policies": [
|
||||||
{
|
{
|
||||||
"users": [
|
"users": [
|
||||||
"spiffe://ddupan.top/ci/panxiao81/gitea-dynamic-runner/publish-images",
|
"spiffe://ddupan.top/ci/panxiao81/backstage/image",
|
||||||
"spiffe://ddupan.top/dev/panxiao81"
|
"spiffe://ddupan.top/dev/panxiao81"
|
||||||
],
|
],
|
||||||
"actions": [
|
"actions": [
|
||||||
@@ -88,18 +88,42 @@ configFiles:
|
|||||||
"read"
|
"read"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"panxiao81/gitea-dynamic-runner-runner": {
|
"panxiao81/gitea-dynamic-runner-controller": {
|
||||||
"policies": [
|
"policies": [
|
||||||
{
|
{
|
||||||
"users": [
|
"users": [
|
||||||
"spiffe://ddupan.top/ci/panxiao81/gitea-dynamic-runner/publish-images",
|
"spiffe://ddupan.top/ci/panxiao81/gitea-dynamic-runner/publish-images"
|
||||||
"spiffe://ddupan.top/dev/panxiao81"
|
|
||||||
],
|
],
|
||||||
"actions": [
|
"actions": [
|
||||||
"read",
|
"read",
|
||||||
"create",
|
"create",
|
||||||
"update"
|
"update"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"users": ["spiffe://ddupan.top/dev/panxiao81"],
|
||||||
|
"actions": ["read", "create", "update", "delete"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"defaultPolicy": [
|
||||||
|
"read"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"panxiao81/gitea-dynamic-runner-runner": {
|
||||||
|
"policies": [
|
||||||
|
{
|
||||||
|
"users": [
|
||||||
|
"spiffe://ddupan.top/ci/panxiao81/gitea-dynamic-runner/publish-images"
|
||||||
|
],
|
||||||
|
"actions": [
|
||||||
|
"read",
|
||||||
|
"create",
|
||||||
|
"update"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"users": ["spiffe://ddupan.top/dev/panxiao81"],
|
||||||
|
"actions": ["read", "create", "update", "delete"]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"defaultPolicy": [
|
"defaultPolicy": [
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
apiVersion: kustomize.toolkit.fluxcd.io/v1
|
||||||
|
kind: Kustomization
|
||||||
|
metadata:
|
||||||
|
name: backstage
|
||||||
|
namespace: flux-system
|
||||||
|
spec:
|
||||||
|
dependsOn:
|
||||||
|
- name: envoy-gateway
|
||||||
|
- name: external-secrets
|
||||||
|
healthChecks:
|
||||||
|
- apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
name: backstage
|
||||||
|
namespace: backstage
|
||||||
|
interval: 10m
|
||||||
|
path: ./apps/backstage
|
||||||
|
prune: true
|
||||||
|
sourceRef:
|
||||||
|
kind: GitRepository
|
||||||
|
name: flux-system
|
||||||
|
timeout: 5m
|
||||||
|
wait: true
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
apiVersion: kustomize.toolkit.fluxcd.io/v1
|
||||||
|
kind: Kustomization
|
||||||
|
metadata:
|
||||||
|
name: hydra
|
||||||
|
namespace: flux-system
|
||||||
|
spec:
|
||||||
|
dependsOn:
|
||||||
|
- name: envoy-gateway
|
||||||
|
- name: external-secrets
|
||||||
|
interval: 10m
|
||||||
|
path: ./apps/hydra
|
||||||
|
prune: false
|
||||||
|
sourceRef:
|
||||||
|
kind: GitRepository
|
||||||
|
name: flux-system
|
||||||
|
timeout: 5m
|
||||||
|
wait: true
|
||||||
@@ -7,6 +7,7 @@ resources:
|
|||||||
- apps/envoy-gateway.yaml
|
- apps/envoy-gateway.yaml
|
||||||
- apps/external-secrets.yaml
|
- apps/external-secrets.yaml
|
||||||
- apps/gitea.yaml
|
- apps/gitea.yaml
|
||||||
|
- apps/backstage.yaml
|
||||||
- apps/http-echo.yaml
|
- apps/http-echo.yaml
|
||||||
- apps/openebs.yaml
|
- apps/openebs.yaml
|
||||||
- apps/nats.yaml
|
- apps/nats.yaml
|
||||||
@@ -15,3 +16,4 @@ resources:
|
|||||||
- apps/observability.yaml
|
- apps/observability.yaml
|
||||||
- apps/zot.yaml
|
- apps/zot.yaml
|
||||||
- apps/nexus.yaml
|
- apps/nexus.yaml
|
||||||
|
- apps/hydra.yaml
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ homelab_dns:
|
|||||||
- { zone: ad.ddupan.top, name: nats, type: A, values: [192.168.10.127] }
|
- { zone: ad.ddupan.top, name: nats, type: A, values: [192.168.10.127] }
|
||||||
- { zone: ad.ddupan.top, name: nexus, type: A, values: [192.168.10.127] }
|
- { zone: ad.ddupan.top, name: nexus, type: A, values: [192.168.10.127] }
|
||||||
- { zone: ad.ddupan.top, name: s3, type: A, values: [192.168.10.127] }
|
- { zone: ad.ddupan.top, name: s3, type: A, values: [192.168.10.127] }
|
||||||
|
- { zone: ad.ddupan.top, name: hydra, type: A, values: [192.168.10.127] }
|
||||||
|
- { zone: ad.ddupan.top, name: hydra-login, type: A, values: [192.168.10.127] }
|
||||||
- { zone: ad.ddupan.top, name: spire-oidc, type: A, values: [192.168.10.127] }
|
- { zone: ad.ddupan.top, name: spire-oidc, type: A, values: [192.168.10.127] }
|
||||||
- { zone: ad.ddupan.top, name: spire-server, type: A, values: [192.168.10.127] }
|
- { zone: ad.ddupan.top, name: spire-server, type: A, values: [192.168.10.127] }
|
||||||
- { zone: ad.ddupan.top, name: zot, type: A, values: [192.168.10.127] }
|
- { zone: ad.ddupan.top, name: zot, type: A, values: [192.168.10.127] }
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
# Gitea dynamic runner
|
# Gitea dynamic runner
|
||||||
|
|
||||||
本目录部署单副本 Go controller,在同一进程运行 RunnerService scheduler、原生
|
本目录部署单副本 Go controller,在同一进程运行 RunnerService scheduler、原生
|
||||||
Kubernetes Pod worker、OpenSandbox VM worker 和 SPIFFE mTLS facade:
|
Kubernetes worker、OpenSandbox worker 和 SPIFFE mTLS facade:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Gitea RunnerService -> scheduler -> JetStream ci.runner.pod
|
Gitea RunnerService -> scheduler -> JetStream ci.runner.container.kubernetes
|
||||||
|
|
|
|
||||||
v
|
v
|
||||||
homelab Kubernetes Pod
|
homelab Kubernetes Pod
|
||||||
@@ -15,7 +15,8 @@ Gitea RunnerService -> scheduler -> JetStream ci.runner.pod
|
|||||||
Gitea
|
Gitea
|
||||||
```
|
```
|
||||||
|
|
||||||
Pod backend 不经过 OpenSandbox。VM backend 后续启用时才访问 VyOS 暴露的
|
`container+kubernetes` placement 不经过 OpenSandbox;`vm+opensandbox` placement
|
||||||
|
访问 VyOS 暴露的
|
||||||
OpenSandbox Lifecycle API;本目录不修改 sandbox 平台侧 ESO、Bao Terraform 或
|
OpenSandbox Lifecycle API;本目录不修改 sandbox 平台侧 ESO、Bao Terraform 或
|
||||||
OpenSandbox chart 所有权边界。
|
OpenSandbox chart 所有权边界。
|
||||||
|
|
||||||
@@ -82,9 +83,9 @@ kubectl -n dynamic-runner rollout status deploy/dynamic-runner-controller --time
|
|||||||
kubectl -n dynamic-runner logs deploy/dynamic-runner-controller -f
|
kubectl -n dynamic-runner logs deploy/dynamic-runner-controller -f
|
||||||
```
|
```
|
||||||
|
|
||||||
确认 scheduler 只领取一条 `[self-hosted,pod]` task,然后验证:
|
确认 scheduler 只领取一条 `[self-hosted,container,kubernetes]` task,然后验证:
|
||||||
|
|
||||||
1. assignment message 进入并离开 durable `pod` consumer;
|
1. assignment message 进入并离开 durable `container.kubernetes` consumer;
|
||||||
2. `gitea-task-<task-id>` Pod 创建,取得实际 Pod UID;
|
2. `gitea-task-<task-id>` Pod 创建,取得实际 Pod UID;
|
||||||
3. 同名 `ClusterStaticEntry` 的 parent ID 包含该 UID,SPIFFE ID 使用 repository/job key;
|
3. 同名 `ClusterStaticEntry` 的 parent ID 包含该 UID,SPIFFE ID 使用 repository/job key;
|
||||||
4. 官方 runner v3.5.0 经 facade claim 精确 task,Gitea 实时收到日志和终态;
|
4. 官方 runner v3.5.0 经 facade claim 精确 task,Gitea 实时收到日志和终态;
|
||||||
@@ -96,7 +97,8 @@ ClusterStaticEntry 全部消失;完整自动清理通过前不得提高 `POD_C
|
|||||||
`VM_CAPACITY`。
|
`VM_CAPACITY`。
|
||||||
|
|
||||||
VM backend 已通过 `vm-dev` canary 完成 Docker、kind、SPIFFE 和完整生命周期测试。
|
VM backend 已通过 `vm-dev` canary 完成 Docker、kind、SPIFFE 和完整生命周期测试。
|
||||||
生产调度标签为 `[self-hosted, vm]`,初始保持 `VM_CAPACITY=1`;扩容前先观察实际任务的
|
生产规范标签为 `[self-hosted, vm, opensandbox]`,旧 `[self-hosted, vm]` 仍严格映射到
|
||||||
|
同一 placement。初始保持 `VM_CAPACITY=1`;扩容前先观察实际任务的
|
||||||
资源水位、等待时间以及 OpenSandbox 是否存在 terminal sandbox 残留。
|
资源水位、等待时间以及 OpenSandbox 是否存在 terminal sandbox 残留。
|
||||||
|
|
||||||
## 回滚
|
## 回滚
|
||||||
|
|||||||
@@ -38,12 +38,12 @@ spec:
|
|||||||
mountPath: /trust
|
mountPath: /trust
|
||||||
containers:
|
containers:
|
||||||
- name: controller
|
- name: controller
|
||||||
image: zot.ad.ddupan.top/panxiao81/gitea-dynamic-runner-controller@sha256:d9dee85b101f94a5a0532ff799241b1916b4765e83f9499abda4095a4d534173
|
image: zot.ad.ddupan.top/panxiao81/gitea-dynamic-runner-controller@sha256:d19f4001e9dfc4ab7d3cdcdfdd758a6b30391f1d29e2fbbe1e39625f86fe740e
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: IfNotPresent
|
||||||
args: [controller]
|
args: [controller]
|
||||||
env:
|
env:
|
||||||
- name: COMPONENTS
|
- name: COMPONENTS
|
||||||
value: scheduler,pod-worker,vm-worker
|
value: scheduler,kubernetes-worker,opensandbox-worker
|
||||||
- name: GITEA_INSTANCE_URL
|
- name: GITEA_INSTANCE_URL
|
||||||
value: https://git.ddupan.top
|
value: https://git.ddupan.top
|
||||||
- name: GITEA_RUNNER_UUID_FILE
|
- name: GITEA_RUNNER_UUID_FILE
|
||||||
@@ -73,7 +73,7 @@ spec:
|
|||||||
fieldRef:
|
fieldRef:
|
||||||
fieldPath: metadata.namespace
|
fieldPath: metadata.namespace
|
||||||
- name: POD_EXECUTOR_IMAGE
|
- name: POD_EXECUTOR_IMAGE
|
||||||
value: zot.ad.ddupan.top/panxiao81/gitea-dynamic-runner-runner@sha256:e7d4bc7d7b56e249a6189d406a7179b83e1626dd66b8ff4de64fe152c9c37d2e
|
value: zot.ad.ddupan.top/panxiao81/gitea-dynamic-runner-runner@sha256:325d75a208b1a1c6f3b4d4705e47bbc58b34733edcd12f689c60769ab6772a59
|
||||||
- name: POD_SERVICE_ACCOUNT
|
- name: POD_SERVICE_ACCOUNT
|
||||||
value: gitea-dynamic-runner
|
value: gitea-dynamic-runner
|
||||||
- name: POD_EXECUTOR_UID
|
- name: POD_EXECUTOR_UID
|
||||||
|
|||||||
@@ -35,10 +35,11 @@ ci_worker_password
|
|||||||
|
|
||||||
## CI stream 约定
|
## CI stream 约定
|
||||||
|
|
||||||
controller 首次启动时幂等创建 `CI_RUNNER` stream:`ci.runner.>`、
|
controller 使用 `CI_RUNNER` stream:`ci.runner.>`、
|
||||||
`WorkQueuePolicy`、file storage、24h/10000 条/256 MiB 上限。每类 runner 使用独立
|
`WorkQueuePolicy`、file storage、24h/10000 条/256 MiB 上限。每类 runner 使用独立
|
||||||
subject 和 durable pull consumer;`ci.runner.<backend>.binding` 传递 runner 实际
|
placement subject 和 durable pull consumer;assignment v2 subject 为
|
||||||
领取任务后的身份绑定。同类型的多个 worker 共享 durable consumer。ACK 后消息立即
|
`ci.runner.<workload-class>.<driver>`,当前为 `ci.runner.container.kubernetes` 与
|
||||||
|
`ci.runner.vm.opensandbox`。同 placement 的多个 worker 共享 durable consumer。ACK 后消息立即
|
||||||
删除,不保存 CI 历史。
|
删除,不保存 CI 历史。
|
||||||
|
|
||||||
## 验证
|
## 验证
|
||||||
|
|||||||
@@ -32,6 +32,14 @@ datasources:
|
|||||||
isDefault: true
|
isDefault: true
|
||||||
jsonData:
|
jsonData:
|
||||||
prometheusType: Prometheus
|
prometheusType: Prometheus
|
||||||
|
- name: Alertmanager
|
||||||
|
uid: alertmanager
|
||||||
|
type: alertmanager
|
||||||
|
access: proxy
|
||||||
|
url: http://vmalertmanager-main.monitoring.svc:9093
|
||||||
|
jsonData:
|
||||||
|
implementation: prometheus
|
||||||
|
handleGrafanaManagedAlerts: false
|
||||||
- name: VictoriaLogs
|
- name: VictoriaLogs
|
||||||
type: victoriametrics-logs-datasource
|
type: victoriametrics-logs-datasource
|
||||||
access: proxy
|
access: proxy
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
apiVersion: external-secrets.io/v1
|
||||||
|
kind: ExternalSecret
|
||||||
|
metadata:
|
||||||
|
name: alertmanager-telegram
|
||||||
|
namespace: monitoring
|
||||||
|
spec:
|
||||||
|
refreshInterval: 1h
|
||||||
|
secretStoreRef:
|
||||||
|
kind: ClusterSecretStore
|
||||||
|
name: openbao
|
||||||
|
target:
|
||||||
|
name: alertmanager-telegram
|
||||||
|
creationPolicy: Owner
|
||||||
|
data:
|
||||||
|
- secretKey: telegram_bot_token
|
||||||
|
remoteRef:
|
||||||
|
key: k8s/alertmanager
|
||||||
|
property: telegram_bot_token
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
apiVersion: helm.toolkit.fluxcd.io/v2
|
||||||
|
kind: HelmRelease
|
||||||
|
metadata:
|
||||||
|
name: kube-state-metrics
|
||||||
|
namespace: monitoring
|
||||||
|
spec:
|
||||||
|
interval: 30m
|
||||||
|
timeout: 5m
|
||||||
|
dependsOn:
|
||||||
|
- name: vm-operator
|
||||||
|
- name: prometheus-operator-crds
|
||||||
|
chart:
|
||||||
|
spec:
|
||||||
|
chart: kube-state-metrics
|
||||||
|
version: 8.5.0
|
||||||
|
sourceRef:
|
||||||
|
kind: HelmRepository
|
||||||
|
name: prometheus-community
|
||||||
|
namespace: monitoring
|
||||||
|
interval: 1h
|
||||||
|
driftDetection:
|
||||||
|
mode: enabled
|
||||||
|
install:
|
||||||
|
remediation:
|
||||||
|
retries: 3
|
||||||
|
upgrade:
|
||||||
|
remediation:
|
||||||
|
retries: 3
|
||||||
|
values:
|
||||||
|
fullnameOverride: kube-state-metrics
|
||||||
|
collectors:
|
||||||
|
- cronjobs
|
||||||
|
- daemonsets
|
||||||
|
- deployments
|
||||||
|
- jobs
|
||||||
|
- namespaces
|
||||||
|
- nodes
|
||||||
|
- persistentvolumeclaims
|
||||||
|
- persistentvolumes
|
||||||
|
- pods
|
||||||
|
- statefulsets
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 128Mi
|
||||||
|
limits:
|
||||||
|
cpu: 500m
|
||||||
|
memory: 384Mi
|
||||||
|
prometheus:
|
||||||
|
monitor:
|
||||||
|
enabled: true
|
||||||
|
jobLabel: app.kubernetes.io/name
|
||||||
|
http:
|
||||||
|
interval: 30s
|
||||||
|
honorLabels: true
|
||||||
|
metrics:
|
||||||
|
interval: 30s
|
||||||
|
honorLabels: true
|
||||||
|
selfMonitor:
|
||||||
|
enabled: true
|
||||||
@@ -6,6 +6,7 @@ resources:
|
|||||||
- vmagent.yaml
|
- vmagent.yaml
|
||||||
- vmalert.yaml
|
- vmalert.yaml
|
||||||
- vmalertmanager.yaml
|
- vmalertmanager.yaml
|
||||||
|
- alertmanager-external-secret.yaml
|
||||||
- reload-rbac.yaml
|
- reload-rbac.yaml
|
||||||
- rules/vm-health.yaml
|
- rules/vm-health.yaml
|
||||||
- rules/vmagent.yaml
|
- rules/vmagent.yaml
|
||||||
@@ -16,3 +17,7 @@ resources:
|
|||||||
- scrapes/kubelet.yaml
|
- scrapes/kubelet.yaml
|
||||||
- exporters/node-exporter.yaml
|
- exporters/node-exporter.yaml
|
||||||
- exporters/process-exporter.yaml
|
- exporters/process-exporter.yaml
|
||||||
|
- exporters/kube-state-metrics-helmrelease.yaml
|
||||||
|
- rules/kubernetes-health.yaml
|
||||||
|
- rules/host-health.yaml
|
||||||
|
- rules/monitoring-delivery.yaml
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
apiVersion: monitoring.coreos.com/v1
|
||||||
|
kind: PrometheusRule
|
||||||
|
metadata:
|
||||||
|
name: host-health
|
||||||
|
namespace: monitoring
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/part-of: victoria-metrics
|
||||||
|
spec:
|
||||||
|
groups:
|
||||||
|
- name: host-health
|
||||||
|
interval: 30s
|
||||||
|
rules:
|
||||||
|
- alert: HostMemoryLow
|
||||||
|
expr: (node_memory_MemAvailable_bytes{job="node-exporter"} / node_memory_MemTotal_bytes{job="node-exporter"})
|
||||||
|
< 0.10
|
||||||
|
for: 10m
|
||||||
|
labels:
|
||||||
|
severity: warning
|
||||||
|
annotations:
|
||||||
|
summary: 主机 {{ $labels.instance }} 可用内存不足 10%
|
||||||
|
description: 检查主机内存、Swap、进程 PSS;仅使用 node-exporter job,避免重复采集统计。
|
||||||
|
runbook_url: https://git.ddupan.top/panxiao81/homelab-wiki/src/branch/main/guides/monitoring-foundation.md
|
||||||
|
- alert: HostFilesystemSpaceLow
|
||||||
|
expr: (node_filesystem_avail_bytes{job="node-exporter",fstype!~"tmpfs|devtmpfs|overlay|squashfs|nsfs|fuse.*",mountpoint!~"/run(/.*)?|/var/lib/kubelet/.*|/var/lib/docker/.*"}
|
||||||
|
/ node_filesystem_size_bytes{job="node-exporter",fstype!~"tmpfs|devtmpfs|overlay|squashfs|nsfs|fuse.*",mountpoint!~"/run(/.*)?|/var/lib/kubelet/.*|/var/lib/docker/.*"}
|
||||||
|
< 0.10) and on(instance,device,mountpoint) (node_filesystem_readonly{job="node-exporter",fstype!~"tmpfs|devtmpfs|overlay|squashfs|nsfs|fuse.*",mountpoint!~"/run(/.*)?|/var/lib/kubelet/.*|/var/lib/docker/.*"}
|
||||||
|
== 0)
|
||||||
|
for: 15m
|
||||||
|
labels:
|
||||||
|
severity: warning
|
||||||
|
annotations:
|
||||||
|
summary: 主机 {{ $labels.instance }} {{ $labels.mountpoint }} 可用空间不足 10%
|
||||||
|
description: 检查文件系统和 ZFS dataset 容量,清理前确认数据归属。
|
||||||
|
runbook_url: https://git.ddupan.top/panxiao81/homelab-wiki/src/branch/main/guides/monitoring-foundation.md
|
||||||
|
- alert: HostFilesystemInodesLow
|
||||||
|
expr: (node_filesystem_files_free{job="node-exporter",fstype!~"tmpfs|devtmpfs|overlay|squashfs|nsfs|fuse.*",mountpoint!~"/run(/.*)?|/var/lib/kubelet/.*|/var/lib/docker/.*"}
|
||||||
|
/ node_filesystem_files{job="node-exporter",fstype!~"tmpfs|devtmpfs|overlay|squashfs|nsfs|fuse.*",mountpoint!~"/run(/.*)?|/var/lib/kubelet/.*|/var/lib/docker/.*"}
|
||||||
|
< 0.10) and on(instance,device,mountpoint) (node_filesystem_files{job="node-exporter",fstype!~"tmpfs|devtmpfs|overlay|squashfs|nsfs|fuse.*",mountpoint!~"/run(/.*)?|/var/lib/kubelet/.*|/var/lib/docker/.*"}
|
||||||
|
> 0) and on(instance,device,mountpoint) (node_filesystem_readonly{job="node-exporter",fstype!~"tmpfs|devtmpfs|overlay|squashfs|nsfs|fuse.*",mountpoint!~"/run(/.*)?|/var/lib/kubelet/.*|/var/lib/docker/.*"}
|
||||||
|
== 0)
|
||||||
|
for: 15m
|
||||||
|
labels:
|
||||||
|
severity: warning
|
||||||
|
annotations:
|
||||||
|
summary: 主机 {{ $labels.instance }} {{ $labels.mountpoint }} inode 不足 10%
|
||||||
|
description: 检查小文件数量,确认 filesystem 是否耗尽 inode。
|
||||||
|
runbook_url: https://git.ddupan.top/panxiao81/homelab-wiki/src/branch/main/guides/monitoring-foundation.md
|
||||||
|
- alert: HostFilesystemReadOnly
|
||||||
|
expr: node_filesystem_readonly{job="node-exporter",fstype!~"tmpfs|devtmpfs|overlay|squashfs|nsfs|fuse.*",mountpoint!~"/run(/.*)?|/var/lib/kubelet/.*|/var/lib/docker/.*"}
|
||||||
|
== 1
|
||||||
|
for: 5m
|
||||||
|
labels:
|
||||||
|
severity: critical
|
||||||
|
annotations:
|
||||||
|
summary: 主机 {{ $labels.instance }} {{ $labels.mountpoint }} 变为只读
|
||||||
|
description: 排查磁盘和文件系统错误;不要直接强制重新挂载。
|
||||||
|
runbook_url: https://git.ddupan.top/panxiao81/homelab-wiki/src/branch/main/guides/monitoring-foundation.md
|
||||||
|
- alert: HostZpoolUnhealthy
|
||||||
|
expr: node_zfs_zpool_state{job="node-exporter",state!="online"} == 1
|
||||||
|
for: 5m
|
||||||
|
labels:
|
||||||
|
severity: critical
|
||||||
|
annotations:
|
||||||
|
summary: 主机 {{ $labels.instance }} ZFS 池 {{ $labels.zpool }} 状态 {{ $labels.state }}
|
||||||
|
description: 检查 zpool status、磁盘和冗余;零值状态不触发。
|
||||||
|
runbook_url: https://git.ddupan.top/panxiao81/homelab-wiki/src/branch/main/guides/monitoring-foundation.md
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
apiVersion: monitoring.coreos.com/v1
|
||||||
|
kind: PrometheusRule
|
||||||
|
metadata:
|
||||||
|
name: kubernetes-health
|
||||||
|
namespace: monitoring
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/part-of: victoria-metrics
|
||||||
|
spec:
|
||||||
|
groups:
|
||||||
|
- name: kubernetes-health
|
||||||
|
interval: 30s
|
||||||
|
rules:
|
||||||
|
- alert: KubeNodeNotReady
|
||||||
|
expr: max by (node) (kube_node_status_condition{job="kube-state-metrics",condition="Ready",status="true"})
|
||||||
|
== 0
|
||||||
|
for: 5m
|
||||||
|
labels:
|
||||||
|
severity: critical
|
||||||
|
annotations:
|
||||||
|
summary: 节点 {{ $labels.node }} 未就绪
|
||||||
|
description: 检查节点、kubelet 和网络;维护时按 node 精确静默。
|
||||||
|
runbook_url: https://git.ddupan.top/panxiao81/homelab-wiki/src/branch/main/guides/monitoring-foundation.md
|
||||||
|
- alert: KubeNodePressure
|
||||||
|
expr: max by (node, condition) (kube_node_status_condition{job="kube-state-metrics",condition=~"MemoryPressure|DiskPressure|PIDPressure",status="true"})
|
||||||
|
== 1
|
||||||
|
for: 5m
|
||||||
|
labels:
|
||||||
|
severity: warning
|
||||||
|
annotations:
|
||||||
|
summary: 节点 {{ $labels.node }} 出现 {{ $labels.condition }}
|
||||||
|
description: 检查内存、磁盘/inode 或 PID 资源压力。
|
||||||
|
runbook_url: https://git.ddupan.top/panxiao81/homelab-wiki/src/branch/main/guides/monitoring-foundation.md
|
||||||
|
- alert: KubePodCrashLooping
|
||||||
|
expr: max by (namespace,pod,container) (kube_pod_container_status_waiting_reason{job="kube-state-metrics",reason="CrashLoopBackOff"})
|
||||||
|
== 1
|
||||||
|
for: 10m
|
||||||
|
labels:
|
||||||
|
severity: warning
|
||||||
|
annotations:
|
||||||
|
summary: 容器 {{ $labels.namespace }}/{{ $labels.pod }}/{{ $labels.container }} 持续崩溃
|
||||||
|
description: 检查容器退出原因和日志;持续 10 分钟后通知,避免短暂重启噪声。
|
||||||
|
runbook_url: https://git.ddupan.top/panxiao81/homelab-wiki/src/branch/main/guides/monitoring-foundation.md
|
||||||
|
- alert: KubePodFrequentRestarts
|
||||||
|
expr: sum by (namespace,pod,container) (increase(kube_pod_container_status_restarts_total{job="kube-state-metrics"}[15m]))
|
||||||
|
> 3
|
||||||
|
for: 5m
|
||||||
|
labels:
|
||||||
|
severity: warning
|
||||||
|
annotations:
|
||||||
|
summary: 容器 {{ $labels.namespace }}/{{ $labels.pod }}/{{ $labels.container }} 频繁重启
|
||||||
|
description: 15 分钟内重启超过 3 次,检查退出原因和资源限制。
|
||||||
|
runbook_url: https://git.ddupan.top/panxiao81/homelab-wiki/src/branch/main/guides/monitoring-foundation.md
|
||||||
|
- alert: KubePodOOMKilled
|
||||||
|
expr: (max by (namespace,pod,container) (kube_pod_container_status_last_terminated_reason{job="kube-state-metrics",reason="OOMKilled"})
|
||||||
|
== 1) and on(namespace,pod,container) (sum by (namespace,pod,container) (increase(kube_pod_container_status_restarts_total{job="kube-state-metrics"}[10m]))
|
||||||
|
> 0)
|
||||||
|
for: 1m
|
||||||
|
labels:
|
||||||
|
severity: warning
|
||||||
|
annotations:
|
||||||
|
summary: 容器 {{ $labels.namespace }}/{{ $labels.pod }}/{{ $labels.container }} 发生 OOM 重启
|
||||||
|
description: 仅在近期有重启且最近终止原因为 OOMKilled 时触发,检查内存峰值和限制。
|
||||||
|
runbook_url: https://git.ddupan.top/panxiao81/homelab-wiki/src/branch/main/guides/monitoring-foundation.md
|
||||||
|
- alert: KubePodPending
|
||||||
|
expr: max by (namespace,pod) (kube_pod_status_phase{job="kube-state-metrics",phase="Pending"}) == 1
|
||||||
|
for: 15m
|
||||||
|
labels:
|
||||||
|
severity: warning
|
||||||
|
annotations:
|
||||||
|
summary: Pod {{ $labels.namespace }}/{{ $labels.pod }} 长时间 Pending
|
||||||
|
description: 检查调度事件、资源、卷和镜像拉取。
|
||||||
|
runbook_url: https://git.ddupan.top/panxiao81/homelab-wiki/src/branch/main/guides/monitoring-foundation.md
|
||||||
|
- alert: KubeDeploymentUnavailable
|
||||||
|
expr: (max by(namespace,deployment) (kube_deployment_spec_replicas{job="kube-state-metrics"}) - max by(namespace,deployment)
|
||||||
|
(kube_deployment_status_replicas_available{job="kube-state-metrics"})) > 0
|
||||||
|
for: 10m
|
||||||
|
labels:
|
||||||
|
severity: warning
|
||||||
|
annotations:
|
||||||
|
summary: Deployment {{ $labels.namespace }}/{{ $labels.deployment }} 副本不足
|
||||||
|
description: 可用副本持续 10 分钟低于声明值;缩容至零不会触发。
|
||||||
|
runbook_url: https://git.ddupan.top/panxiao81/homelab-wiki/src/branch/main/guides/monitoring-foundation.md
|
||||||
|
- alert: KubeStatefulSetUnavailable
|
||||||
|
expr: (max by(namespace,statefulset) (kube_statefulset_replicas{job="kube-state-metrics"}) - max by(namespace,statefulset)
|
||||||
|
(kube_statefulset_status_replicas_ready{job="kube-state-metrics"})) > 0
|
||||||
|
for: 10m
|
||||||
|
labels:
|
||||||
|
severity: warning
|
||||||
|
annotations:
|
||||||
|
summary: StatefulSet {{ $labels.namespace }}/{{ $labels.statefulset }} 副本不足
|
||||||
|
description: 检查 Pod 就绪、存储和依赖;已知维护按工作负载精确静默。
|
||||||
|
runbook_url: https://git.ddupan.top/panxiao81/homelab-wiki/src/branch/main/guides/monitoring-foundation.md
|
||||||
|
- alert: KubeDaemonSetUnavailable
|
||||||
|
expr: (max by(namespace,daemonset) (kube_daemonset_status_desired_number_scheduled{job="kube-state-metrics"})
|
||||||
|
- max by(namespace,daemonset) (kube_daemonset_status_number_ready{job="kube-state-metrics"})) > 0
|
||||||
|
for: 10m
|
||||||
|
labels:
|
||||||
|
severity: warning
|
||||||
|
annotations:
|
||||||
|
summary: DaemonSet {{ $labels.namespace }}/{{ $labels.daemonset }} 副本不足
|
||||||
|
description: 检查节点和 Pod 就绪;不按整个 namespace 屏蔽。
|
||||||
|
runbook_url: https://git.ddupan.top/panxiao81/homelab-wiki/src/branch/main/guides/monitoring-foundation.md
|
||||||
|
- alert: KubePVCPending
|
||||||
|
expr: max by(namespace,persistentvolumeclaim) (kube_persistentvolumeclaim_status_phase{job="kube-state-metrics",phase="Pending"})
|
||||||
|
== 1
|
||||||
|
for: 15m
|
||||||
|
labels:
|
||||||
|
severity: warning
|
||||||
|
annotations:
|
||||||
|
summary: PVC {{ $labels.namespace }}/{{ $labels.persistentvolumeclaim }} 长时间 Pending
|
||||||
|
description: 检查 StorageClass、调度拓扑、容量与 provisioner。
|
||||||
|
runbook_url: https://git.ddupan.top/panxiao81/homelab-wiki/src/branch/main/guides/monitoring-foundation.md
|
||||||
|
- alert: KubeJobFailed
|
||||||
|
expr: max by(namespace,job_name) (kube_job_failed{job="kube-state-metrics",condition="true"}) == 1
|
||||||
|
for: 5m
|
||||||
|
labels:
|
||||||
|
severity: warning
|
||||||
|
annotations:
|
||||||
|
summary: Job {{ $labels.namespace }}/{{ $labels.job_name }} 已失败
|
||||||
|
description: 检查失败 Job 的 Pod 和任务日志;成功重试中的失败 Pod 计数不作为失败 Job。
|
||||||
|
runbook_url: https://git.ddupan.top/panxiao81/homelab-wiki/src/branch/main/guides/monitoring-foundation.md
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
apiVersion: monitoring.coreos.com/v1
|
||||||
|
kind: PrometheusRule
|
||||||
|
metadata:
|
||||||
|
name: monitoring-delivery
|
||||||
|
namespace: monitoring
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/part-of: victoria-metrics
|
||||||
|
spec:
|
||||||
|
groups:
|
||||||
|
- name: monitoring-delivery
|
||||||
|
interval: 30s
|
||||||
|
rules:
|
||||||
|
- alert: KubeStateMetricsUnavailable
|
||||||
|
expr: up{job="kube-state-metrics"} == 0 or absent(up{job="kube-state-metrics"})
|
||||||
|
for: 5m
|
||||||
|
labels:
|
||||||
|
severity: critical
|
||||||
|
annotations:
|
||||||
|
summary: kube-state-metrics 采集不可用
|
||||||
|
description: 检查 HelmRelease、ServiceMonitor 转换、targets 与 exporter;目标消失也会触发。
|
||||||
|
runbook_url: https://git.ddupan.top/panxiao81/homelab-wiki/src/branch/main/guides/monitoring-foundation.md
|
||||||
|
- alert: NodeExporterUnavailable
|
||||||
|
expr: up{job="node-exporter"} == 0 or absent(up{job="node-exporter"})
|
||||||
|
for: 5m
|
||||||
|
labels:
|
||||||
|
severity: critical
|
||||||
|
annotations:
|
||||||
|
summary: node-exporter 采集不可用
|
||||||
|
description: 检查节点与 exporter,主机健康规则依赖此采集。
|
||||||
|
runbook_url: https://git.ddupan.top/panxiao81/homelab-wiki/src/branch/main/guides/monitoring-foundation.md
|
||||||
|
- alert: NatsMetricsUnavailable
|
||||||
|
expr: up{job="nats/nats"} == 0 or absent(up{job="nats/nats"})
|
||||||
|
for: 5m
|
||||||
|
labels:
|
||||||
|
severity: warning
|
||||||
|
annotations:
|
||||||
|
summary: NATS 指标采集不可用
|
||||||
|
description: 依次检查 PodMonitor、转换后的 VMPodScrape、:7777 exporter 及 targets。
|
||||||
|
runbook_url: https://git.ddupan.top/panxiao81/homelab-wiki/src/branch/main/guides/monitoring-foundation.md
|
||||||
|
- alert: AlertmanagerTelegramDeliveryFailed
|
||||||
|
expr: sum by(job,instance,integration) (increase(alertmanager_notifications_failed_total{job="vmalertmanager-main",integration="telegram"}[5m]))
|
||||||
|
> 0
|
||||||
|
for: 1m
|
||||||
|
labels:
|
||||||
|
severity: critical
|
||||||
|
annotations:
|
||||||
|
summary: Alertmanager 向 Telegram 发送失败
|
||||||
|
description: 检查网络、bot token 和频道权限。若 Telegram 完全不可达,本告警也无法经同一渠道送达;需在 Alertmanager/Grafana 排查,集群外心跳另行建设。
|
||||||
|
runbook_url: https://git.ddupan.top/panxiao81/homelab-wiki/src/branch/main/guides/monitoring-foundation.md
|
||||||
|
- alert: AlertmanagerConfigurationReloadFailed
|
||||||
|
expr: alertmanager_config_last_reload_successful{job="vmalertmanager-main"} == 0
|
||||||
|
for: 5m
|
||||||
|
labels:
|
||||||
|
severity: critical
|
||||||
|
annotations:
|
||||||
|
summary: Alertmanager 配置加载失败
|
||||||
|
description: 检查 operator 和 Alertmanager 日志、配置格式及 Secret 引用。
|
||||||
|
runbook_url: https://git.ddupan.top/panxiao81/homelab-wiki/src/branch/main/guides/monitoring-foundation.md
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# Pull metrics from Docker hosts running ../../docker-hosts/compose.yaml.
|
# 仅抓取已存在的宿主机 node-exporter。旧 :8080 目标实际返回 nginx 404,
|
||||||
# vmagent scrapes each host's node-exporter (:9100) and cAdvisor (:8080) over the LAN.
|
# 独立 Docker cAdvisor 尚未部署;部署并确认端口后再添加其目标。
|
||||||
# Add one target block per host; keep the `host` label in sync with its HOST_LABEL.
|
# Kubernetes 容器指标由 kubelet.yaml 的 /metrics/cadvisor 独立采集。
|
||||||
apiVersion: operator.victoriametrics.com/v1beta1
|
apiVersion: operator.victoriametrics.com/v1beta1
|
||||||
kind: VMStaticScrape
|
kind: VMStaticScrape
|
||||||
metadata:
|
metadata:
|
||||||
@@ -17,13 +17,6 @@ spec:
|
|||||||
labels:
|
labels:
|
||||||
job: node-exporter
|
job: node-exporter
|
||||||
host: docker-01
|
host: docker-01
|
||||||
- targets:
|
# --- 新增主机前确认 exporter 已部署且 /metrics 返回成功 ---
|
||||||
- "192.168.10.127:8080"
|
|
||||||
labels:
|
|
||||||
job: cadvisor
|
|
||||||
host: docker-01
|
|
||||||
# --- add more hosts below, mirroring the two blocks above ---
|
|
||||||
# - targets: ["192.168.10.x:9100"]
|
# - targets: ["192.168.10.x:9100"]
|
||||||
# labels: { job: node-exporter, host: docker-02 }
|
# labels: { job: node-exporter, host: docker-02 }
|
||||||
# - targets: ["192.168.10.x:8080"]
|
|
||||||
# labels: { job: cadvisor, host: docker-02 }
|
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ spec:
|
|||||||
honorLabels: true
|
honorLabels: true
|
||||||
honorTimestamps: false
|
honorTimestamps: false
|
||||||
interval: 30s
|
interval: 30s
|
||||||
|
# k3s 此端点包含 apiserver/etcd 指标,响应已超过默认 16 MiB。
|
||||||
|
# 仅放宽 kubelet 任务,其他采集目标保留默认限制。
|
||||||
|
max_scrape_size: "32MiB"
|
||||||
tlsConfig:
|
tlsConfig:
|
||||||
insecureSkipVerify: true
|
insecureSkipVerify: true
|
||||||
caFile: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
|
caFile: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# 依赖 Python 3 + PyYAML,以及 PATH 中的 promtool(验证版本 3.5.0)。
|
||||||
|
set -euo pipefail
|
||||||
|
test_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
check_dir="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$check_dir"' EXIT
|
||||||
|
python3 - "$test_dir/../rules" "$check_dir/rules.yaml" <<'PY'
|
||||||
|
import pathlib
|
||||||
|
import sys
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
groups = []
|
||||||
|
for name in ("kubernetes-health", "host-health", "monitoring-delivery"):
|
||||||
|
path = pathlib.Path(sys.argv[1]) / f"{name}.yaml"
|
||||||
|
groups.extend(yaml.safe_load(path.read_text())["spec"]["groups"])
|
||||||
|
pathlib.Path(sys.argv[2]).write_text(yaml.safe_dump({"groups": groups}, allow_unicode=True))
|
||||||
|
PY
|
||||||
|
cp "$test_dir/foundation.test.yaml" "$check_dir/foundation.test.yaml"
|
||||||
|
promtool check rules "$check_dir/rules.yaml"
|
||||||
|
promtool test rules "$check_dir/foundation.test.yaml"
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
rule_files:
|
||||||
|
- rules.yaml
|
||||||
|
evaluation_interval: 1m
|
||||||
|
tests:
|
||||||
|
- name: 健康 ZFS 的零值故障状态不报警
|
||||||
|
interval: 1m
|
||||||
|
input_series:
|
||||||
|
- series: node_zfs_zpool_state{job="node-exporter",instance="laptop",zpool="data",state="degraded"}
|
||||||
|
values: '0x20'
|
||||||
|
- series: node_zfs_zpool_state{job="node-exporter",instance="laptop",zpool="data",state="online"}
|
||||||
|
values: 1x20
|
||||||
|
alert_rule_test:
|
||||||
|
- eval_time: 10m
|
||||||
|
alertname: HostZpoolUnhealthy
|
||||||
|
exp_alerts: &id001 []
|
||||||
|
- name: ZFS 降级持续五分钟触发
|
||||||
|
interval: 1m
|
||||||
|
input_series:
|
||||||
|
- series: node_zfs_zpool_state{job="node-exporter",instance="laptop",zpool="data",state="degraded"}
|
||||||
|
values: 1x20
|
||||||
|
alert_rule_test:
|
||||||
|
- eval_time: 4m
|
||||||
|
alertname: HostZpoolUnhealthy
|
||||||
|
exp_alerts: *id001
|
||||||
|
- eval_time: 6m
|
||||||
|
alertname: HostZpoolUnhealthy
|
||||||
|
exp_alerts:
|
||||||
|
- exp_labels:
|
||||||
|
job: node-exporter
|
||||||
|
instance: laptop
|
||||||
|
zpool: data
|
||||||
|
state: degraded
|
||||||
|
severity: critical
|
||||||
|
exp_annotations:
|
||||||
|
summary: 主机 laptop ZFS 池 data 状态 degraded
|
||||||
|
description: 检查 zpool status、磁盘和冗余;零值状态不触发。
|
||||||
|
runbook_url: https://git.ddupan.top/panxiao81/homelab-wiki/src/branch/main/guides/monitoring-foundation.md
|
||||||
|
- name: 忽略旧 docker-hosts 重复主机指标
|
||||||
|
interval: 1m
|
||||||
|
input_series:
|
||||||
|
- series: node_memory_MemAvailable_bytes{job="node-exporter",instance="laptop"}
|
||||||
|
values: 50x20
|
||||||
|
- series: node_memory_MemTotal_bytes{job="node-exporter",instance="laptop"}
|
||||||
|
values: 100x20
|
||||||
|
- series: node_memory_MemAvailable_bytes{job="docker-hosts",instance="laptop"}
|
||||||
|
values: 1x20
|
||||||
|
- series: node_memory_MemTotal_bytes{job="docker-hosts",instance="laptop"}
|
||||||
|
values: 100x20
|
||||||
|
alert_rule_test:
|
||||||
|
- eval_time: 15m
|
||||||
|
alertname: HostMemoryLow
|
||||||
|
exp_alerts: *id001
|
||||||
|
- name: 临时文件系统耗尽不触发持久磁盘告警
|
||||||
|
interval: 1m
|
||||||
|
input_series:
|
||||||
|
- series: node_filesystem_avail_bytes{job="node-exporter",instance="laptop",device="tmpfs",mountpoint="/run",fstype="tmpfs"}
|
||||||
|
values: 1x20
|
||||||
|
- series: node_filesystem_size_bytes{job="node-exporter",instance="laptop",device="tmpfs",mountpoint="/run",fstype="tmpfs"}
|
||||||
|
values: 100x20
|
||||||
|
- series: node_filesystem_readonly{job="node-exporter",instance="laptop",device="tmpfs",mountpoint="/run",fstype="tmpfs"}
|
||||||
|
values: '0x20'
|
||||||
|
alert_rule_test:
|
||||||
|
- eval_time: 20m
|
||||||
|
alertname: HostFilesystemSpaceLow
|
||||||
|
exp_alerts: *id001
|
||||||
|
- name: Deployment 允许短暂滚动且缩零不报错
|
||||||
|
interval: 1m
|
||||||
|
input_series:
|
||||||
|
- series: kube_deployment_spec_replicas{job="kube-state-metrics",namespace="app",deployment="web"}
|
||||||
|
values: 2x20
|
||||||
|
- series: kube_deployment_status_replicas_available{job="kube-state-metrics",namespace="app",deployment="web"}
|
||||||
|
values: 1x20
|
||||||
|
- series: kube_deployment_spec_replicas{job="kube-state-metrics",namespace="app",deployment="scaled-down"}
|
||||||
|
values: '0x20'
|
||||||
|
- series: kube_deployment_status_replicas_available{job="kube-state-metrics",namespace="app",deployment="scaled-down"}
|
||||||
|
values: '0x20'
|
||||||
|
alert_rule_test:
|
||||||
|
- eval_time: 5m
|
||||||
|
alertname: KubeDeploymentUnavailable
|
||||||
|
exp_alerts: *id001
|
||||||
|
- eval_time: 11m
|
||||||
|
alertname: KubeDeploymentUnavailable
|
||||||
|
exp_alerts:
|
||||||
|
- exp_labels:
|
||||||
|
namespace: app
|
||||||
|
deployment: web
|
||||||
|
severity: warning
|
||||||
|
exp_annotations:
|
||||||
|
summary: Deployment app/web 副本不足
|
||||||
|
description: 可用副本持续 10 分钟低于声明值;缩容至零不会触发。
|
||||||
|
runbook_url: https://git.ddupan.top/panxiao81/homelab-wiki/src/branch/main/guides/monitoring-foundation.md
|
||||||
|
- name: NATS 发现集合完全消失也报警
|
||||||
|
interval: 1m
|
||||||
|
input_series: []
|
||||||
|
alert_rule_test:
|
||||||
|
- eval_time: 6m
|
||||||
|
alertname: NatsMetricsUnavailable
|
||||||
|
exp_alerts:
|
||||||
|
- exp_labels:
|
||||||
|
job: nats/nats
|
||||||
|
severity: warning
|
||||||
|
exp_annotations:
|
||||||
|
summary: NATS 指标采集不可用
|
||||||
|
description: 依次检查 PodMonitor、转换后的 VMPodScrape、:7777 exporter 及 targets。
|
||||||
|
runbook_url: https://git.ddupan.top/panxiao81/homelab-wiki/src/branch/main/guides/monitoring-foundation.md
|
||||||
|
- name: 历史 OOM 没有新重启不反复报警
|
||||||
|
interval: 1m
|
||||||
|
input_series:
|
||||||
|
- series: kube_pod_container_status_last_terminated_reason{job="kube-state-metrics",namespace="app",pod="web",container="web",reason="OOMKilled"}
|
||||||
|
values: 1x20
|
||||||
|
- series: kube_pod_container_status_restarts_total{job="kube-state-metrics",namespace="app",pod="web",container="web"}
|
||||||
|
values: 5x20
|
||||||
|
alert_rule_test:
|
||||||
|
- eval_time: 10m
|
||||||
|
alertname: KubePodOOMKilled
|
||||||
|
exp_alerts: *id001
|
||||||
|
- name: Telegram 失败跨 reason 汇总为一条
|
||||||
|
interval: 1m
|
||||||
|
input_series:
|
||||||
|
- series: alertmanager_notifications_failed_total{job="vmalertmanager-main",instance="am",integration="telegram",reason="clientError"}
|
||||||
|
values: 0+1x20
|
||||||
|
- series: alertmanager_notifications_failed_total{job="vmalertmanager-main",instance="am",integration="telegram",reason="serverError"}
|
||||||
|
values: 0+1x20
|
||||||
|
alert_rule_test:
|
||||||
|
- eval_time: 6m
|
||||||
|
alertname: AlertmanagerTelegramDeliveryFailed
|
||||||
|
exp_alerts:
|
||||||
|
- exp_labels:
|
||||||
|
job: vmalertmanager-main
|
||||||
|
instance: am
|
||||||
|
integration: telegram
|
||||||
|
severity: critical
|
||||||
|
exp_annotations:
|
||||||
|
summary: Alertmanager 向 Telegram 发送失败
|
||||||
|
description: 检查网络、bot token 和频道权限。若 Telegram 完全不可达,本告警也无法经同一渠道送达;需在 Alertmanager/Grafana
|
||||||
|
排查,集群外心跳另行建设。
|
||||||
|
runbook_url: https://git.ddupan.top/panxiao81/homelab-wiki/src/branch/main/guides/monitoring-foundation.md
|
||||||
@@ -10,6 +10,11 @@ spec:
|
|||||||
replicaCount: 1
|
replicaCount: 1
|
||||||
selectAllByDefault: true
|
selectAllByDefault: true
|
||||||
evaluationInterval: 30s
|
evaluationInterval: 30s
|
||||||
|
# 告警 Source 复用已认证的 Grafana 入口,携带表达式和触发时间。
|
||||||
|
# 整个 Explore JSON 一次性 URL 编码,避免表达式中的引号、&、+ 损坏链接。
|
||||||
|
extraArgs:
|
||||||
|
external.url: https://grafana.ad.ddupan.top
|
||||||
|
external.alert.source: 'explore?left={{ printf "{\"datasource\":\"VictoriaMetrics\",\"queries\":[{\"expr\":%s,\"refId\":\"A\"}],\"range\":{\"from\":\"%d\",\"to\":\"now\"}}" (.Expr | jsonEscape) .ActiveAt.UnixMilli | queryEscape }}'
|
||||||
datasource:
|
datasource:
|
||||||
url: http://vmsingle-main.monitoring.svc:8428
|
url: http://vmsingle-main.monitoring.svc:8428
|
||||||
remoteWrite:
|
remoteWrite:
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
# Alert router/notifier. Migrated from the retired Compose stack (see Git history),
|
# Telegram token 由 ExternalSecret 从 OpenBao 投射,配置中仅引用文件。
|
||||||
# which currently blackholes everything. Wire real receivers here (email via the
|
|
||||||
# in-cluster smtp-relay, or a webhook) when you want notifications.
|
|
||||||
apiVersion: operator.victoriametrics.com/v1beta1
|
apiVersion: operator.victoriametrics.com/v1beta1
|
||||||
kind: VMAlertmanager
|
kind: VMAlertmanager
|
||||||
metadata:
|
metadata:
|
||||||
@@ -8,19 +6,37 @@ metadata:
|
|||||||
namespace: monitoring
|
namespace: monitoring
|
||||||
spec:
|
spec:
|
||||||
replicaCount: 1
|
replicaCount: 1
|
||||||
|
secrets:
|
||||||
|
- alertmanager-telegram
|
||||||
configRawYaml: |
|
configRawYaml: |
|
||||||
route:
|
route:
|
||||||
receiver: blackhole
|
receiver: blackhole
|
||||||
|
group_by: [alertname, cluster, job, severity]
|
||||||
|
group_wait: 1m
|
||||||
|
group_interval: 5m
|
||||||
|
repeat_interval: 4h
|
||||||
|
routes:
|
||||||
|
- receiver: telegram
|
||||||
|
matchers:
|
||||||
|
- severity="critical"
|
||||||
|
group_wait: 10s
|
||||||
|
repeat_interval: 1h
|
||||||
|
- receiver: telegram
|
||||||
|
matchers:
|
||||||
|
- severity="warning"
|
||||||
|
inhibit_rules:
|
||||||
|
- source_matchers:
|
||||||
|
- alertname="KubePodCrashLooping"
|
||||||
|
target_matchers:
|
||||||
|
- alertname="KubePodFrequentRestarts"
|
||||||
|
equal: [namespace, pod, container]
|
||||||
receivers:
|
receivers:
|
||||||
- name: blackhole
|
- name: blackhole
|
||||||
# Example email receiver via the in-cluster Postfix relay (smtp-relay/):
|
- name: telegram
|
||||||
# receivers:
|
telegram_configs:
|
||||||
# - name: email
|
- bot_token_file: /etc/vm/secrets/alertmanager-telegram/telegram_bot_token
|
||||||
# email_configs:
|
chat_id: -1003956377923
|
||||||
# - to: '[email protected]'
|
send_resolved: true
|
||||||
# from: 'Alertmanager <[email protected]>'
|
|
||||||
# smarthost: 'smtp-relay.smtp-relay.svc.cluster.local:25'
|
|
||||||
# require_tls: false
|
|
||||||
resources:
|
resources:
|
||||||
requests:
|
requests:
|
||||||
cpu: 25m
|
cpu: 25m
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ metadata:
|
|||||||
name: vm-operator
|
name: vm-operator
|
||||||
namespace: monitoring
|
namespace: monitoring
|
||||||
spec:
|
spec:
|
||||||
|
dependsOn:
|
||||||
|
- name: prometheus-operator-crds
|
||||||
chart:
|
chart:
|
||||||
spec:
|
spec:
|
||||||
chart: victoria-metrics-operator
|
chart: victoria-metrics-operator
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ spec:
|
|||||||
mountPath: /opt/opensandbox
|
mountPath: /opt/opensandbox
|
||||||
containers:
|
containers:
|
||||||
- name: sandbox
|
- name: sandbox
|
||||||
image: zot.ad.ddupan.top/panxiao81/gitea-dynamic-runner-runner@sha256:e7d4bc7d7b56e249a6189d406a7179b83e1626dd66b8ff4de64fe152c9c37d2e
|
image: zot.ad.ddupan.top/panxiao81/gitea-dynamic-runner-runner@sha256:325d75a208b1a1c6f3b4d4705e47bbc58b34733edcd12f689c60769ab6772a59
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: IfNotPresent
|
||||||
command: [/opt/opensandbox/task-executor]
|
command: [/opt/opensandbox/task-executor]
|
||||||
args:
|
args:
|
||||||
@@ -86,6 +86,8 @@ spec:
|
|||||||
mountPath: /opt/opensandbox
|
mountPath: /opt/opensandbox
|
||||||
- name: runner-data
|
- name: runner-data
|
||||||
mountPath: /data
|
mountPath: /data
|
||||||
|
- name: runner-workspace
|
||||||
|
mountPath: /workspace
|
||||||
- name: spire-socket
|
- name: spire-socket
|
||||||
mountPath: /run/spire/agent-sockets
|
mountPath: /run/spire/agent-sockets
|
||||||
- name: spire-agent
|
- name: spire-agent
|
||||||
@@ -114,6 +116,8 @@ spec:
|
|||||||
emptyDir: {}
|
emptyDir: {}
|
||||||
- name: runner-data
|
- name: runner-data
|
||||||
emptyDir: {}
|
emptyDir: {}
|
||||||
|
- name: runner-workspace
|
||||||
|
emptyDir: {}
|
||||||
- name: spire-data
|
- name: spire-data
|
||||||
emptyDir:
|
emptyDir:
|
||||||
medium: Memory
|
medium: Memory
|
||||||
@@ -182,7 +186,7 @@ spec:
|
|||||||
mountPath: /opt/opensandbox
|
mountPath: /opt/opensandbox
|
||||||
containers:
|
containers:
|
||||||
- name: sandbox
|
- name: sandbox
|
||||||
image: zot.ad.ddupan.top/panxiao81/gitea-dynamic-runner-runner@sha256:e7d4bc7d7b56e249a6189d406a7179b83e1626dd66b8ff4de64fe152c9c37d2e
|
image: zot.ad.ddupan.top/panxiao81/gitea-dynamic-runner-runner@sha256:325d75a208b1a1c6f3b4d4705e47bbc58b34733edcd12f689c60769ab6772a59
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: IfNotPresent
|
||||||
command: [/opt/opensandbox/task-executor]
|
command: [/opt/opensandbox/task-executor]
|
||||||
args:
|
args:
|
||||||
@@ -217,6 +221,8 @@ spec:
|
|||||||
mountPath: /opt/opensandbox
|
mountPath: /opt/opensandbox
|
||||||
- name: runner-data
|
- name: runner-data
|
||||||
mountPath: /data
|
mountPath: /data
|
||||||
|
- name: runner-workspace
|
||||||
|
mountPath: /workspace
|
||||||
- name: docker-socket
|
- name: docker-socket
|
||||||
mountPath: /run/docker
|
mountPath: /run/docker
|
||||||
- name: spire-socket
|
- name: spire-socket
|
||||||
@@ -259,6 +265,8 @@ spec:
|
|||||||
emptyDir: {}
|
emptyDir: {}
|
||||||
- name: runner-data
|
- name: runner-data
|
||||||
emptyDir: {}
|
emptyDir: {}
|
||||||
|
- name: runner-workspace
|
||||||
|
emptyDir: {}
|
||||||
- name: docker-data
|
- name: docker-data
|
||||||
emptyDir: {}
|
emptyDir: {}
|
||||||
- name: docker-socket
|
- name: docker-socket
|
||||||
|
|||||||
Reference in New Issue
Block a user