This commit is contained in:
26
.gitea/workflows/build.yml
Normal file
26
.gitea/workflows/build.yml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
name: Docker Image CI
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: controller
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Build the Docker image
|
||||||
|
image: harbor.merlin.xin/mirrors/gcr.io/kaniko-project/executor:debug
|
||||||
|
run: |
|
||||||
|
for df in $(find . -name Dockerfile); do
|
||||||
|
dir=$(dirname "$df")
|
||||||
|
image_name="${vars.REGISTRY_URL}/testing/merlin/$(basename $dir):${gitea.sha:0:7}"
|
||||||
|
/kaniko/executor \
|
||||||
|
--dockerfile "$df" \
|
||||||
|
--context "$dir" \
|
||||||
|
--destination "$image_name" \
|
||||||
|
--cache=true \
|
||||||
|
--username "${secrets.HARBOR_USER_NAME}" \
|
||||||
|
--password "${secrets.HARBOR_USER_SECRET}"
|
||||||
|
echo "✅ Successfully built and pushed $image_name"
|
||||||
|
done
|
||||||
26
tls-sync/Dockerfile
Normal file
26
tls-sync/Dockerfile
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
# 安装依赖 如果有需要,可以取消注释安装网络调试工具
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
# openssh-client \
|
||||||
|
# curl \
|
||||||
|
# iputils-ping \
|
||||||
|
# netcat-openbsd \
|
||||||
|
# dnsutils \
|
||||||
|
# telnet \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# 安装 Python 依赖
|
||||||
|
RUN pip install --no-cache-dir kubernetes pycryptodome
|
||||||
|
|
||||||
|
# 复制脚本
|
||||||
|
COPY job.py /app/job.py
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 设置环境变量(可在部署时覆盖)
|
||||||
|
ENV NGINX_HOST=10.0.0.1
|
||||||
|
ENV NGINX_USER=nginx
|
||||||
|
ENV NAMESPACE=basic
|
||||||
|
|
||||||
|
# 默认命令
|
||||||
|
CMD ["python", "job.py"]
|
||||||
8
tls-sync/change_log/0.md
Normal file
8
tls-sync/change_log/0.md
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# 0.0.0
|
||||||
|
基础版本,监听目标namespace证书的生成
|
||||||
|
针对集群网关为traefik
|
||||||
|
|
||||||
|
# 0.0.1 - 0.0.4
|
||||||
|
切换针对集群内部网关为nginx的配置
|
||||||
|
监听所有namespace的证书的生成
|
||||||
|
|
||||||
213
tls-sync/job.py
Normal file
213
tls-sync/job.py
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import base64
|
||||||
|
import logging
|
||||||
|
import subprocess
|
||||||
|
from kubernetes import client, config, watch
|
||||||
|
from kubernetes.client.rest import ApiException
|
||||||
|
from Crypto.PublicKey import RSA
|
||||||
|
|
||||||
|
# 配置日志
|
||||||
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||||
|
|
||||||
|
# 配置 Kubernetes 客户端
|
||||||
|
try:
|
||||||
|
config.load_incluster_config() # 如果在 Pod 内运行
|
||||||
|
except Exception as e:
|
||||||
|
logging.error("Failed to load kube config: %s", e)
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
v1 = client.CoreV1Api()
|
||||||
|
|
||||||
|
# 配置 Nginx 主机和目录
|
||||||
|
CLUSTER_HOST = "10.0.0.4"
|
||||||
|
NGINX_HOST = "10.0.0.1"
|
||||||
|
NGINX_USER = "nginx"
|
||||||
|
NGINX_CERT_DIR = "/etc/nginx/certs"
|
||||||
|
NGINX_CONF_DIR = "/etc/nginx/conf.d"
|
||||||
|
NAMESPACE = "basic"
|
||||||
|
MAX_RETRIES = 10 # 最大重试次数
|
||||||
|
RETRY_DELAY = 20 # 每次重试的时间间隔(秒)
|
||||||
|
|
||||||
|
# 创建目录用于存放临时证书和密钥
|
||||||
|
TEMP_DIR = "/tmp/nginx_certs"
|
||||||
|
RSA_DIR = "/root/.ssh" # 新增的文件夹,用于存放 SSH 密钥对
|
||||||
|
PUBLIC_KEY_COMMENT = "generated-key"
|
||||||
|
os.makedirs(TEMP_DIR, exist_ok=True)
|
||||||
|
os.makedirs(RSA_DIR, exist_ok=True)
|
||||||
|
|
||||||
|
def generate_ssh_key_pair():
|
||||||
|
"""
|
||||||
|
生成 SSH 密钥对(私钥为 PEM,公钥为 OpenSSH 单行格式),并保存到 RSA_DIR。
|
||||||
|
私钥权限设为 600,公钥权限设为 644。
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
logging.info("Generating SSH key pair...")
|
||||||
|
|
||||||
|
os.makedirs(RSA_DIR, exist_ok=True)
|
||||||
|
|
||||||
|
# 生成 RSA 私钥
|
||||||
|
key = RSA.generate(2048)
|
||||||
|
|
||||||
|
# 私钥(PEM,多行)
|
||||||
|
private_key_bytes = key.export_key(format='PEM')
|
||||||
|
# 公钥(OpenSSH 单行格式,例如: b"ssh-rsa AAAAB3NzaC1yc2E..."}
|
||||||
|
public_key_bytes = key.publickey().export_key(format='OpenSSH')
|
||||||
|
|
||||||
|
# 给公钥添加注释(可选),并确保以换行结尾
|
||||||
|
if PUBLIC_KEY_COMMENT:
|
||||||
|
public_key_bytes = public_key_bytes + b' ' + PUBLIC_KEY_COMMENT.encode('utf-8')
|
||||||
|
public_key_bytes = public_key_bytes + b'\n'
|
||||||
|
|
||||||
|
private_key_path = os.path.join(RSA_DIR, "id_rsa")
|
||||||
|
public_key_path = os.path.join(RSA_DIR, "id_rsa.pub")
|
||||||
|
|
||||||
|
# 写入私钥与公钥
|
||||||
|
with open(private_key_path, "wb") as private_file:
|
||||||
|
private_file.write(private_key_bytes)
|
||||||
|
|
||||||
|
with open(public_key_path, "wb") as public_file:
|
||||||
|
public_file.write(public_key_bytes)
|
||||||
|
|
||||||
|
# 设置合理的文件权限
|
||||||
|
try:
|
||||||
|
os.chmod(private_key_path, 0o600)
|
||||||
|
os.chmod(public_key_path, 0o644)
|
||||||
|
except PermissionError:
|
||||||
|
logging.warning("No permission to chmod the key files; please set permissions manually if needed.")
|
||||||
|
|
||||||
|
logging.info(f"SSH key pair saved to {private_key_path} and {public_key_path}")
|
||||||
|
except Exception as e:
|
||||||
|
logging.error("Failed to generate SSH key pair: %s", e)
|
||||||
|
|
||||||
|
def upload_cert_with_retry(tmp_crt, tmp_key, domain):
|
||||||
|
"""
|
||||||
|
尝试多次上传证书文件到 Nginx 服务器,直到成功或达到最大重试次数
|
||||||
|
"""
|
||||||
|
retries = 0
|
||||||
|
while retries < MAX_RETRIES:
|
||||||
|
try:
|
||||||
|
logging.info(f"Attempting to upload certificate for {domain}, attempt {retries + 1}...")
|
||||||
|
|
||||||
|
# 上传证书和私钥,使用 ssh + sudo tee
|
||||||
|
for local_file, remote_file in [
|
||||||
|
(tmp_crt, f"{NGINX_CERT_DIR}/{domain}.pem"),
|
||||||
|
(tmp_key, f"{NGINX_CERT_DIR}/{domain}.key")
|
||||||
|
]:
|
||||||
|
ssh_command = [
|
||||||
|
"ssh", f"{NGINX_USER}@{NGINX_HOST}",
|
||||||
|
f"sudo tee {remote_file} > /dev/null"
|
||||||
|
]
|
||||||
|
with open(local_file, "rb") as f:
|
||||||
|
subprocess.run(ssh_command, stdin=f, check=True)
|
||||||
|
|
||||||
|
logging.info(f"Uploaded certificate and key for {domain} to Nginx server.")
|
||||||
|
return # 上传成功,退出循环
|
||||||
|
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
retries += 1
|
||||||
|
logging.error(f"Attempt {retries} failed: {e}. Retrying in {RETRY_DELAY} seconds...")
|
||||||
|
time.sleep(RETRY_DELAY)
|
||||||
|
|
||||||
|
logging.error(f"Failed to upload certificate for {domain} after {MAX_RETRIES} attempts.")
|
||||||
|
|
||||||
|
|
||||||
|
def upload_cert(domain, crt_data, key_data):
|
||||||
|
"""
|
||||||
|
上传证书和私钥到 Nginx 中转服务器,并生成 Nginx 配置文件
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 临时保存证书和私钥
|
||||||
|
tmp_crt = os.path.join(TEMP_DIR, f"{domain}.crt")
|
||||||
|
tmp_key = os.path.join(TEMP_DIR, f"{domain}.key")
|
||||||
|
|
||||||
|
with open(tmp_crt, "wb") as f:
|
||||||
|
f.write(base64.b64decode(crt_data))
|
||||||
|
with open(tmp_key, "wb") as f:
|
||||||
|
f.write(base64.b64decode(key_data))
|
||||||
|
|
||||||
|
logging.info(f"Certificate and key for {domain} saved locally.")
|
||||||
|
|
||||||
|
# 上传证书和私钥
|
||||||
|
upload_cert_with_retry(tmp_crt, tmp_key, domain)
|
||||||
|
|
||||||
|
# 生成 Nginx 配置文件
|
||||||
|
conf_content = f"""
|
||||||
|
server {{
|
||||||
|
listen 443 ssl http2;
|
||||||
|
server_name {domain};
|
||||||
|
|
||||||
|
ssl_certificate /etc/nginx/certs/{domain}.pem;
|
||||||
|
ssl_certificate_key /etc/nginx/certs/{domain}.key;
|
||||||
|
|
||||||
|
location / {{
|
||||||
|
proxy_pass https://{CLUSTER_HOST};
|
||||||
|
proxy_ssl_server_name on;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
tmp_conf = os.path.join(TEMP_DIR, f"{domain}.conf")
|
||||||
|
with open(tmp_conf, "w") as f:
|
||||||
|
f.write(conf_content)
|
||||||
|
|
||||||
|
# 上传 Nginx 配置文件
|
||||||
|
ssh_command = [
|
||||||
|
"ssh", f"{NGINX_USER}@{NGINX_HOST}",
|
||||||
|
f"sudo tee {NGINX_CONF_DIR}/{domain}.conf > /dev/null"
|
||||||
|
]
|
||||||
|
logging.info(f"Uploading Nginx config for {domain} using sudo tee...")
|
||||||
|
with open(tmp_conf, "rb") as f:
|
||||||
|
subprocess.run(ssh_command, stdin=f, check=True)
|
||||||
|
|
||||||
|
logging.info(f"Uploaded Nginx config for {domain} to Nginx server via sudo tee.")
|
||||||
|
|
||||||
|
# 重载 Nginx
|
||||||
|
reload_command = ["ssh", f"{NGINX_USER}@{NGINX_HOST}", "sudo nginx -s reload"]
|
||||||
|
subprocess.run(reload_command, check=True)
|
||||||
|
logging.info(f"Nginx reloaded successfully for {domain}.")
|
||||||
|
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
logging.error("Error during SSH commands: %s", e)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error("Unexpected error: %s", e)
|
||||||
|
|
||||||
|
def watch_secrets_all_ns():
|
||||||
|
"""
|
||||||
|
监听所有命名空间中的 Secrets,当发现以 -tls 结尾的 cert-manager 生成证书时上传到 Nginx
|
||||||
|
"""
|
||||||
|
logging.info("Starting to watch all Kubernetes secrets across namespaces.")
|
||||||
|
|
||||||
|
w = watch.Watch()
|
||||||
|
try:
|
||||||
|
# 监听所有命名空间
|
||||||
|
for event in w.stream(v1.list_secret_for_all_namespaces):
|
||||||
|
secret = event['object']
|
||||||
|
secret_name = secret.metadata.name
|
||||||
|
annotations = secret.metadata.annotations or {}
|
||||||
|
|
||||||
|
# 仅处理 cert-manager 生成的 TLS Secret
|
||||||
|
if secret_name.endswith("-tls") and annotations.get("cert-manager.io/issuer-name"):
|
||||||
|
if "tls.crt" in secret.data and "tls.key" in secret.data:
|
||||||
|
domain = secret_name[:-4] # 去掉 -tls 后缀
|
||||||
|
logging.info(f"Found TLS secret {secret_name} for domain {domain}, uploading...")
|
||||||
|
|
||||||
|
upload_cert(domain, secret.data["tls.crt"], secret.data["tls.key"])
|
||||||
|
|
||||||
|
except ApiException as e:
|
||||||
|
logging.error("Kubernetes API exception: %s", e)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error("Unexpected error: %s", e)
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# 生成 SSH 密钥对
|
||||||
|
generate_ssh_key_pair()
|
||||||
|
|
||||||
|
# 开始监听 Secrets 变化
|
||||||
|
watch_secrets_all_ns()
|
||||||
42
tls-sync/readme.md
Normal file
42
tls-sync/readme.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# 使用说明
|
||||||
|
建议直接拉取镜像:
|
||||||
|
```
|
||||||
|
docker pull forever4526/tls:{tag}
|
||||||
|
```
|
||||||
|
编写yaml后,使用kubectl apply应用
|
||||||
|
|
||||||
|
# 镜像运行必备环境变量
|
||||||
|
|
||||||
|
**中转服务器与pod相关**
|
||||||
|
CLUSTER_HOST = "10.0.0.4" //集群相对ip
|
||||||
|
NGINX_HOST = "10.0.0.1" //nginx主机地址
|
||||||
|
NGINX_USER = "nginx" //ssh链接nginx主机的用户名
|
||||||
|
NGINX_CERT_DIR = "/etc/nginx/certs" //存放证书的目录
|
||||||
|
NGINX_CONF_DIR = "/etc/nginx/conf.d" //存放server配置块的目录
|
||||||
|
NAMESPACE = "basic" //pod运行的命名空间
|
||||||
|
TEMP_DIR = "/tmp/nginx_certs" //临时目录
|
||||||
|
RSA_DIR = "/root/.ssh" //存放 SSH 密钥对
|
||||||
|
PUBLIC_KEY_COMMENT = "generated-key" //ssh公钥后缀
|
||||||
|
|
||||||
|
**上传重试相关**
|
||||||
|
MAX_RETRIES = 10 // 最大重试次数
|
||||||
|
RETRY_DELAY = 20 // 每次重试的时间间隔(秒)
|
||||||
|
|
||||||
|
# 创建serviceAccount供其使用
|
||||||
|
关键:监听单namespace
|
||||||
|
```
|
||||||
|
rules:
|
||||||
|
- apiGroups: [""]
|
||||||
|
resources: ["secrets"]
|
||||||
|
verbs: ["watch", "get", "list"]
|
||||||
|
```
|
||||||
|
|
||||||
|
监听全部namespace需要更高的权限,default用户就无法使用了,关键的rules是一样的,只不过需要自己创建用户,绑定权限,部署时绑定创建的用户即可
|
||||||
|
|
||||||
|
# 请注意
|
||||||
|
|
||||||
|
**部署完成之后,需要进入容器,手动复制ssh公钥到中转服务器的对应用户下的.ssh/authorized_keys中**
|
||||||
|
|
||||||
|
**配置完公钥之后,需要在pod中手动进行一次ssh远程连接,目的是为了验证指纹信息(目前我还没自动实现)**
|
||||||
|
|
||||||
|
**可以使用固定的ssh密钥对,只是我的环境走docker hub中转镜像比较方便,所以就写了一个通用的(个人感觉麻烦程度差不多)**
|
||||||
Reference in New Issue
Block a user