cleanup(ingress): drop legacy Caddy/cloudflared/netbird-gateway tiers #70
@@ -1,35 +0,0 @@
|
||||
when:
|
||||
- event: push
|
||||
path: ".woodpecker/build-caddy.yml"
|
||||
- event: manual
|
||||
evaluate: 'PIPELINE == "build-caddy"'
|
||||
|
||||
steps:
|
||||
- name: build
|
||||
image: gitea.renner.uno/gitea_admin/ci-caddy-builder:2026-05-01
|
||||
commands:
|
||||
- xcaddy build v2.11.2
|
||||
--with github.com/caddy-dns/cloudflare
|
||||
--with github.com/ss098/certmagic-s3
|
||||
--with github.com/hslatman/caddy-crowdsec-bouncer/http@v0.10.0
|
||||
--with github.com/hslatman/caddy-crowdsec-bouncer/appsec@v0.10.0
|
||||
--with github.com/mholt/caddy-l4
|
||||
--output caddy
|
||||
- ./caddy version
|
||||
- ./caddy list-modules | grep -c layer4
|
||||
|
||||
- name: release
|
||||
image: gitea.renner.uno/gitea_admin/ci-k8s:2026-04-29
|
||||
environment:
|
||||
GITEA_TOKEN:
|
||||
from_secret: gitea_token
|
||||
commands:
|
||||
# Delete old asset from 'latest' release, then upload new one
|
||||
- "RELEASE=$(curl -s -H \"Authorization: token $GITEA_TOKEN\" https://gitea.renner.uno/api/v1/repos/gitea_admin/homelab/releases/tags/latest)"
|
||||
- "RELEASE_ID=$(echo $RELEASE | jq -r '.id')"
|
||||
- "echo \"Release ID: $RELEASE_ID\""
|
||||
# Delete existing caddy asset if present
|
||||
- "ASSET_ID=$(echo $RELEASE | jq -r '.assets[]? | select(.name==\"caddy\") | .id')"
|
||||
- "[ -n \"$ASSET_ID\" ] && curl -s -X DELETE -H \"Authorization: token $GITEA_TOKEN\" https://gitea.renner.uno/api/v1/repos/gitea_admin/homelab/releases/$RELEASE_ID/assets/$ASSET_ID || true"
|
||||
# Upload new binary
|
||||
- "curl -s -X POST -H \"Authorization: token $GITEA_TOKEN\" -F attachment=@caddy https://gitea.renner.uno/api/v1/repos/gitea_admin/homelab/releases/$RELEASE_ID/assets?name=caddy | jq '{name: .name, size: .size}'"
|
||||
@@ -67,26 +67,6 @@ steps:
|
||||
--destination gitea.renner.uno/gitea_admin/ci-tofu:$TAG \
|
||||
--destination gitea.renner.uno/gitea_admin/ci-tofu:latest
|
||||
|
||||
- name: build-caddy-builder
|
||||
image: gcr.io/kaniko-project/executor:v1.23.2-debug
|
||||
directory: ci/images/caddy-builder
|
||||
depends_on: []
|
||||
environment:
|
||||
GITEA_USER: gitea_admin
|
||||
GITEA_TOKEN:
|
||||
from_secret: gitea_token
|
||||
commands:
|
||||
- mkdir -p /kaniko/.docker
|
||||
- AUTH=$(printf '%s:%s' "$GITEA_USER" "$GITEA_TOKEN" | base64 | tr -d '\n')
|
||||
- printf '{"auths":{"gitea.renner.uno":{"auth":"%s"}}}' "$AUTH" > /kaniko/.docker/config.json
|
||||
- TAG=$(date +%Y-%m-%d)
|
||||
- |
|
||||
/kaniko/executor \
|
||||
--context . \
|
||||
--dockerfile Containerfile \
|
||||
--destination gitea.renner.uno/gitea_admin/ci-caddy-builder:$TAG \
|
||||
--destination gitea.renner.uno/gitea_admin/ci-caddy-builder:latest
|
||||
|
||||
- name: build-claude
|
||||
image: gcr.io/kaniko-project/executor:v1.23.2-debug
|
||||
directory: ci/images/claude
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
when:
|
||||
- event: push
|
||||
path:
|
||||
- "ansible/roles/caddy-ha/**"
|
||||
- "ansible/plays/install-caddy-ha.yml"
|
||||
- ".woodpecker/deploy-caddy.yml"
|
||||
- event: manual
|
||||
evaluate: 'PIPELINE == "deploy-caddy"'
|
||||
|
||||
timeout: 30
|
||||
|
||||
steps:
|
||||
- name: deploy-caddy
|
||||
image: gitea.renner.uno/gitea_admin/ci-ansible:2026-04-29
|
||||
environment:
|
||||
ANSIBLE_FORCE_COLOR: "true"
|
||||
ANSIBLE_HOST_KEY_CHECKING: "false"
|
||||
ANSIBLE_ROLES_PATH: "../roles:./roles"
|
||||
SSH_PRIVATE_KEY:
|
||||
from_secret: ssh_private_key
|
||||
VAULT_PASS:
|
||||
from_secret: vault_pass
|
||||
PROXMOX_TOKEN_ID:
|
||||
from_secret: proxmox_token_id
|
||||
PROXMOX_TOKEN_SECRET:
|
||||
from_secret: proxmox_token_secret
|
||||
commands:
|
||||
- mkdir -p ~/.ssh
|
||||
- echo "$SSH_PRIVATE_KEY" | base64 -d > ~/.ssh/id_ed25519
|
||||
- chmod 600 ~/.ssh/id_ed25519
|
||||
- printf "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null\n" > ~/.ssh/config
|
||||
- cd ansible
|
||||
- echo "$VAULT_PASS" > .vault_pass && chmod 600 .vault_pass
|
||||
- export ANSIBLE_VAULT_PASSWORD_FILE=.vault_pass
|
||||
- ansible-playbook -i inventory/ plays/install-caddy-ha.yml -v
|
||||
@@ -1,4 +0,0 @@
|
||||
---
|
||||
# NetBird client configuration for Caddy HA servers
|
||||
# Setup key is stored in vault.yml
|
||||
netbird_setup_key: "{{ vault_caddy_netbird_setup_key }}"
|
||||
@@ -1,9 +0,0 @@
|
||||
$ANSIBLE_VAULT;1.1;AES256
|
||||
37363434633432396531333038633664663632333963646630353239316332646638323364383162
|
||||
3338306237343739306231613164383064383037616463320a363961313964326438373762666365
|
||||
35313464333033653130316539356431303063323861333734313766373061323365613431343966
|
||||
3766636265626530370a643261653134313563663035623164353566653139383832313161386562
|
||||
62376364336565313739306130336434323638306137326263663030393633333831646562623138
|
||||
37313937363338393161313964396462346264633339636464663539383034373837376430366237
|
||||
36626664623762623130316163373933383335396463616161333434303863636330633439376131
|
||||
62393862393065396238
|
||||
@@ -1,18 +0,0 @@
|
||||
- name: Configure Caddy High Availability
|
||||
hosts: proxmox_caddy_ha
|
||||
serial: 1
|
||||
become: true
|
||||
|
||||
tasks:
|
||||
- name: Apply Caddy High Availability Role
|
||||
import_role:
|
||||
name: caddy-ha
|
||||
vars:
|
||||
caddy_ha_vip: 10.2.45.100
|
||||
caddy_ha_keepalived_auth_pass: "SecureCaddyHA"
|
||||
caddy_ha_primary_host: caddy.dmz.renner.uno
|
||||
caddy_ha_priorities:
|
||||
"caddy.dmz.renner.uno": 100
|
||||
"caddy2.dmz.renner.uno": 90
|
||||
"caddy3.dmz.renner.uno": 80
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
- name: Setup Cloudflared LXC
|
||||
hosts: proxmox_cloudflared
|
||||
remote_user: root
|
||||
become: true
|
||||
roles:
|
||||
- cloudflared-lxc
|
||||
@@ -1,25 +0,0 @@
|
||||
---
|
||||
- name: Deploy CrowdSec on Caddy HA servers
|
||||
hosts: proxmox_caddy_ha
|
||||
serial: 1
|
||||
become: true
|
||||
vars_files:
|
||||
- vars/rustfs_vault.yml
|
||||
- vars/caddy_setup_credentials.yml
|
||||
- [ "vars/caddy_generated_credentials.yml", "vars/empty.yml" ]
|
||||
|
||||
tasks:
|
||||
- name: Install and configure CrowdSec
|
||||
import_role:
|
||||
name: crowdsec
|
||||
|
||||
- name: Apply Caddy High Availability Role with CrowdSec
|
||||
import_role:
|
||||
name: caddy-ha
|
||||
vars:
|
||||
caddy_ha_vip: 10.2.45.100
|
||||
caddy_ha_keepalived_auth_pass: "SecureCaddyHA"
|
||||
caddy_ha_primary_host: caddy.srv.renner.uno
|
||||
crowdsec_enabled: true
|
||||
crowdsec_lapi_url: "http://127.0.0.1:8080"
|
||||
crowdsec_appsec_url: "http://127.0.0.1:7422"
|
||||
@@ -1,17 +0,0 @@
|
||||
---
|
||||
# Deploy NetBird Gateway to Homelab LXC
|
||||
# Usage: ansible-playbook -i inventory/ plays/install-netbird-gateway.yml
|
||||
#
|
||||
# Prerequisites:
|
||||
# - LXC 260 created via OpenTofu
|
||||
# - NetBird server running at netbird.renner.uno
|
||||
# - Setup key generated from NetBird management UI
|
||||
# - Vault secrets configured:
|
||||
# - netbird_gateway_setup_key
|
||||
|
||||
- name: Deploy NetBird Gateway
|
||||
hosts: netbird-gateway.srv.renner.uno
|
||||
become: true
|
||||
roles:
|
||||
- common
|
||||
- netbird-gateway
|
||||
@@ -1,18 +0,0 @@
|
||||
---
|
||||
caddy_ha_s3_access_key: !vault |
|
||||
$ANSIBLE_VAULT;1.1;AES256
|
||||
62306337353164323163363334613963396563333133313634316164643632646537386463393730
|
||||
3934363964613331326663363762303833393766353537650a663139623966316362646437653362
|
||||
62356231623736333735643639326434333737323737623336366433373433643733323463396132
|
||||
6230626633666137370a316239363837303038633738376531373261303633343635323737323732
|
||||
6166
|
||||
|
||||
caddy_ha_s3_secret_key: !vault |
|
||||
$ANSIBLE_VAULT;1.1;AES256
|
||||
35353431396362333634613134643733383562366230303035383563653131383134376238373539
|
||||
3765623535373439343637373733303531653565663561380a306364303962613265623363623231
|
||||
31303430646131313737306361343661373238346166386562636361383437376561373161303234
|
||||
6164313036613065370a393662373466326361396134633434633034616235386366323132373964
|
||||
37386361373262396438396633613865643235346563396434336330636330643438383638343661
|
||||
3365333063663835393635306439613137313161343232333565
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
$ANSIBLE_VAULT;1.1;AES256
|
||||
66343132346639356335353436323062303930326233386334653465663837326237643939623132
|
||||
6431623632316361656564333137323038323034326365370a396262643664396533616139623162
|
||||
64626635626364383561623964373236316335343633616230353530633933356661326532616666
|
||||
3639663032636630330a653264623562303433343830663437313239373135643362373864396537
|
||||
62323733373461336164323864346530386262623764386433326163663561626466666634326234
|
||||
33356639373364393431373236656537613434323638333033336137306535326133636666373565
|
||||
66333363316339376131633434373435343139393666623264366463653262346237333564643536
|
||||
39636335376330383133333062616637656530333366633266663531386537626163646566303637
|
||||
64653834653439316133323034303764643535353165343635343539613162636635
|
||||
@@ -1,37 +0,0 @@
|
||||
---
|
||||
caddy_ha_primary_host: caddy.dmz.renner.uno
|
||||
caddy_ha_vip: 10.2.45.100
|
||||
caddy_ha_keepalived_auth_pass: "SecureCaddyHA"
|
||||
caddy_ha_keepalived_interface: "{{ ansible_default_ipv4.interface | default('ens18') }}"
|
||||
|
||||
# Keepalived priorities per host (higher = more preferred)
|
||||
caddy_ha_priorities:
|
||||
"caddy.dmz.renner.uno": 100
|
||||
"caddy2.dmz.renner.uno": 90
|
||||
"caddy3.dmz.renner.uno": 80
|
||||
|
||||
# RustFS / S3 Storage for Certificates
|
||||
caddy_ha_s3_endpoint: "s3.renner.uno:9000"
|
||||
caddy_ha_s3_bucket: "caddy-certificates"
|
||||
# Default credentials for Caddy (will be overridden by vault or setup script)
|
||||
# caddy_ha_s3_access_key: ""
|
||||
# caddy_ha_s3_secret_key: ""
|
||||
caddy_ha_s3_insecure: true
|
||||
|
||||
# Admin credentials for setup (should be loaded from secure vars/vault)
|
||||
# caddy_ha_s3_admin_access_key: "..."
|
||||
# caddy_ha_s3_admin_secret_key: "..."
|
||||
|
||||
caddy_version: "v2.11.2"
|
||||
|
||||
# Caddy Binary Download URL
|
||||
# Replace 'owner' with the user/org where the caddy-builder repo is located
|
||||
caddy_ha_caddy_binary_url: "https://git.renner.uno/gitea_admin/homelab/releases/download/latest/caddy"
|
||||
|
||||
# CrowdSec Integration
|
||||
crowdsec_enabled: true
|
||||
crowdsec_lapi_url: "http://127.0.0.1:8080"
|
||||
crowdsec_appsec_url: "http://127.0.0.1:7422"
|
||||
crowdsec_ticker_interval: "15s"
|
||||
# API key is set per-host from the crowdsec role output
|
||||
# crowdsec_caddy_api_key: ""
|
||||
@@ -1,199 +0,0 @@
|
||||
import boto3
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
import subprocess
|
||||
import requests
|
||||
from botocore.client import Config
|
||||
from botocore.auth import SigV4Auth
|
||||
from botocore.awsrequest import AWSRequest
|
||||
from botocore.credentials import Credentials
|
||||
from botocore.exceptions import ClientError
|
||||
from urllib.parse import urlparse
|
||||
|
||||
endpoint = os.environ.get("S3_ENDPOINT")
|
||||
admin_access_key = os.environ.get("S3_ADMIN_ACCESS_KEY")
|
||||
admin_secret_key = os.environ.get("S3_ADMIN_SECRET_KEY")
|
||||
existing_access_key = os.environ.get("S3_EXISTING_ACCESS_KEY")
|
||||
existing_secret_key = os.environ.get("S3_EXISTING_SECRET_KEY")
|
||||
bucket_name = os.environ.get("S3_BUCKET")
|
||||
insecure = os.environ.get("S3_INSECURE", "false").lower() == "true"
|
||||
vault_output_file = os.environ.get("S3_VAULT_FILE")
|
||||
target_user = "caddy-ha"
|
||||
|
||||
if not endpoint.startswith("http"):
|
||||
endpoint = f"http://{endpoint}" if insecure else f"https://{endpoint}"
|
||||
|
||||
# Security Check: Ensure existing keys are not the admin keys
|
||||
if existing_access_key and existing_access_key == admin_access_key:
|
||||
sys.stderr.write("Warning: Provided existing access key matches admin access key. Ignoring to prevent using admin credentials for Caddy.\n")
|
||||
existing_access_key = None
|
||||
existing_secret_key = None
|
||||
|
||||
# Test existing keys if provided
|
||||
if existing_access_key and existing_secret_key:
|
||||
try:
|
||||
s3_check = boto3.client('s3',
|
||||
endpoint_url=endpoint,
|
||||
aws_access_key_id=existing_access_key,
|
||||
aws_secret_access_key=existing_secret_key,
|
||||
config=Config(signature_version='s3v4'),
|
||||
region_name='us-east-1',
|
||||
verify=not insecure)
|
||||
s3_check.head_bucket(Bucket=bucket_name)
|
||||
# If successful, return existing keys
|
||||
print(json.dumps({
|
||||
"bucket_created": False,
|
||||
"access_key": existing_access_key,
|
||||
"secret_key": existing_secret_key,
|
||||
"used_existing": True
|
||||
}))
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
sys.stderr.write(f"Warning: Existing keys invalid or insufficient permissions: {e}. Generating new ones.\n")
|
||||
|
||||
# Clients (Admin)
|
||||
s3 = boto3.client('s3',
|
||||
endpoint_url=endpoint,
|
||||
aws_access_key_id=admin_access_key,
|
||||
aws_secret_access_key=admin_secret_key,
|
||||
config=Config(signature_version='s3v4'),
|
||||
region_name='us-east-1',
|
||||
verify=not insecure)
|
||||
|
||||
# RustFS Admin API helper
|
||||
def rustfs_admin_request(method, path, params=None, data=None):
|
||||
import hashlib
|
||||
if params is None:
|
||||
params = {}
|
||||
|
||||
url = f"{endpoint}{path}"
|
||||
|
||||
# Calculate Payload Hash
|
||||
content = data.encode('utf-8') if data else b''
|
||||
payload_hash = hashlib.sha256(content).hexdigest()
|
||||
|
||||
headers = {}
|
||||
if data:
|
||||
headers["Content-Type"] = "application/json"
|
||||
headers["x-amz-content-sha256"] = payload_hash
|
||||
|
||||
request = AWSRequest(
|
||||
method=method,
|
||||
url=url,
|
||||
data=data,
|
||||
params=params,
|
||||
headers=headers
|
||||
)
|
||||
|
||||
# Sign Request
|
||||
creds = Credentials(admin_access_key, admin_secret_key)
|
||||
auth = SigV4Auth(creds, "s3", "us-east-1")
|
||||
auth.add_auth(request)
|
||||
|
||||
# Send Request
|
||||
response = requests.request(
|
||||
method=method,
|
||||
url=request.url,
|
||||
headers=dict(request.headers),
|
||||
data=data,
|
||||
params=params,
|
||||
verify=not insecure
|
||||
)
|
||||
return response
|
||||
|
||||
def ensure_bucket():
|
||||
sys.stderr.write(f"Checking bucket {bucket_name}...\n")
|
||||
try:
|
||||
s3.head_bucket(Bucket=bucket_name)
|
||||
sys.stderr.write(f"Bucket {bucket_name} exists.\n")
|
||||
except ClientError as e:
|
||||
error_code = int(e.response['Error']['Code'])
|
||||
if error_code == 404:
|
||||
sys.stderr.write(f"Bucket {bucket_name} not found. Creating...\n")
|
||||
s3.create_bucket(Bucket=bucket_name)
|
||||
sys.stderr.write(f"Bucket {bucket_name} created.\n")
|
||||
else:
|
||||
raise
|
||||
|
||||
def generate_random_key(length=20):
|
||||
import secrets
|
||||
import string
|
||||
alphabet = string.ascii_letters + string.digits
|
||||
return ''.join(secrets.choice(alphabet) for i in range(length))
|
||||
|
||||
def ensure_user_and_keys():
|
||||
# RustFS specific implementation
|
||||
|
||||
new_secret_key = generate_random_key(40)
|
||||
|
||||
payload = json.dumps({
|
||||
"secretKey": new_secret_key,
|
||||
"status": "enabled"
|
||||
})
|
||||
|
||||
# 1. Add User (or update password)
|
||||
sys.stderr.write(f"Updating user {target_user}...\n")
|
||||
resp = rustfs_admin_request("PUT", "/rustfs/admin/v3/add-user", params={"accessKey": target_user}, data=payload)
|
||||
|
||||
if resp.status_code != 200:
|
||||
sys.stderr.write(f"Warning: Failed to create/update user via RustFS API: {resp.status_code} {resp.text}\n")
|
||||
return None, None
|
||||
|
||||
# 2. Set Policy (Assign 'readwrite' canned policy)
|
||||
# Note: Ideally we should create a custom policy restricted to the bucket,
|
||||
# but for now we use 'readwrite' to ensure functionality.
|
||||
sys.stderr.write(f"Setting policy for user {target_user}...\n")
|
||||
resp_policy = rustfs_admin_request("PUT", "/rustfs/admin/v3/set-user-or-group-policy",
|
||||
params={
|
||||
"userOrGroup": target_user,
|
||||
"policyName": "readwrite",
|
||||
"isGroup": "false"
|
||||
}
|
||||
)
|
||||
|
||||
if resp_policy.status_code != 200:
|
||||
sys.stderr.write(f"Warning: Failed to set policy for user: {resp_policy.status_code} {resp_policy.text}\n")
|
||||
# Proceed anyway? Or fail? The user might not work.
|
||||
# We'll return keys but warn.
|
||||
|
||||
return target_user, new_secret_key
|
||||
|
||||
def encrypt_string(value, var_name):
|
||||
cmd = ["ansible-vault", "encrypt_string", value, "--name", var_name]
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
if result.returncode != 0:
|
||||
raise Exception(f"Failed to encrypt string: {result.stderr}")
|
||||
return result.stdout
|
||||
|
||||
try:
|
||||
ensure_bucket()
|
||||
ak, sk = ensure_user_and_keys()
|
||||
|
||||
if ak and sk and vault_output_file:
|
||||
# Write to vault file
|
||||
with open(vault_output_file, "w") as f:
|
||||
f.write("---\n")
|
||||
f.write(encrypt_string(ak, "caddy_ha_s3_access_key"))
|
||||
f.write("\n")
|
||||
f.write(encrypt_string(sk, "caddy_ha_s3_secret_key"))
|
||||
f.write("\n")
|
||||
print(f"Credentials encrypted and written to {vault_output_file}", file=sys.stderr)
|
||||
elif not ak or not sk:
|
||||
raise Exception("Failed to generate access keys")
|
||||
|
||||
result = {
|
||||
"bucket_created": True,
|
||||
"access_key": ak,
|
||||
"secret_key": sk,
|
||||
"used_existing": False
|
||||
}
|
||||
|
||||
print(json.dumps(result))
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.stderr.write(f"Error: {e}\n")
|
||||
sys.exit(1)
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
---
|
||||
- name: Restart Keepalived
|
||||
service:
|
||||
name: keepalived
|
||||
state: restarted
|
||||
|
||||
- name: Reload Caddy
|
||||
service:
|
||||
name: caddy
|
||||
state: restarted
|
||||
@@ -1,201 +0,0 @@
|
||||
---
|
||||
- name: Determine HA State
|
||||
set_fact:
|
||||
keepalived_state: "{{ 'MASTER' if inventory_hostname == caddy_ha_primary_host else 'BACKUP' }}"
|
||||
keepalived_priority: "{{ caddy_ha_priorities[inventory_hostname] | default(80) }}"
|
||||
|
||||
- name: Install dependencies
|
||||
apt:
|
||||
pkg:
|
||||
- debian-keyring
|
||||
- debian-archive-keyring
|
||||
- apt-transport-https
|
||||
- keepalived
|
||||
- curl
|
||||
- tar
|
||||
- libcap2-bin
|
||||
state: present
|
||||
update_cache: yes
|
||||
register: result
|
||||
|
||||
- name: Ensure caddy group exists
|
||||
group:
|
||||
name: caddy
|
||||
system: yes
|
||||
state: present
|
||||
|
||||
- name: Ensure caddy user exists
|
||||
user:
|
||||
name: caddy
|
||||
group: caddy
|
||||
system: yes
|
||||
home: /var/lib/caddy
|
||||
shell: /usr/sbin/nologin
|
||||
create_home: yes
|
||||
state: present
|
||||
|
||||
- name: Download Caddy Custom Binary
|
||||
get_url:
|
||||
url: "{{ caddy_ha_caddy_binary_url }}"
|
||||
dest: /usr/bin/caddy
|
||||
mode: '0755'
|
||||
owner: root
|
||||
group: root
|
||||
notify: Reload Caddy
|
||||
register: caddy_download
|
||||
ignore_errors: true
|
||||
|
||||
- name: Build Caddy with xcaddy (Fallback)
|
||||
when: caddy_download is failed
|
||||
block:
|
||||
- name: Install Build Dependencies (git, curl)
|
||||
apt:
|
||||
pkg:
|
||||
- curl
|
||||
- git
|
||||
- tar
|
||||
state: present
|
||||
update_cache: yes
|
||||
|
||||
- name: Download Go
|
||||
get_url:
|
||||
url: "https://go.dev/dl/go1.24.0.linux-amd64.tar.gz"
|
||||
dest: "/tmp/go1.24.0.linux-amd64.tar.gz"
|
||||
|
||||
- name: Remove existing Go installation
|
||||
file:
|
||||
path: /usr/local/go
|
||||
state: absent
|
||||
|
||||
- name: Extract Go
|
||||
unarchive:
|
||||
src: "/tmp/go1.24.0.linux-amd64.tar.gz"
|
||||
dest: "/usr/local"
|
||||
remote_src: yes
|
||||
|
||||
- name: Link Go binary
|
||||
file:
|
||||
src: /usr/local/go/bin/go
|
||||
dest: /usr/bin/go
|
||||
state: link
|
||||
force: yes
|
||||
|
||||
- name: Install xcaddy (Add GPG key)
|
||||
get_url:
|
||||
url: https://dl.cloudsmith.io/public/caddy/xcaddy/gpg.key
|
||||
dest: /usr/share/keyrings/caddy-xcaddy-archive-keyring.asc
|
||||
mode: '0644'
|
||||
|
||||
- name: De-armor GPG key
|
||||
shell: cat /usr/share/keyrings/caddy-xcaddy-archive-keyring.asc | gpg --dearmor -o /usr/share/keyrings/caddy-xcaddy-archive-keyring.gpg
|
||||
args:
|
||||
creates: /usr/share/keyrings/caddy-xcaddy-archive-keyring.gpg
|
||||
|
||||
- name: Install xcaddy (Add Repository)
|
||||
copy:
|
||||
content: "deb [signed-by=/usr/share/keyrings/caddy-xcaddy-archive-keyring.gpg] https://dl.cloudsmith.io/public/caddy/xcaddy/deb/debian any-version main"
|
||||
dest: /etc/apt/sources.list.d/caddy-xcaddy.list
|
||||
|
||||
- name: Install xcaddy
|
||||
apt:
|
||||
name: xcaddy
|
||||
state: present
|
||||
update_cache: yes
|
||||
|
||||
- name: Build Caddy
|
||||
command:
|
||||
cmd: >
|
||||
xcaddy build {{ caddy_version }}
|
||||
--output /usr/bin/caddy
|
||||
--with github.com/caddy-dns/cloudflare
|
||||
--with github.com/ss098/certmagic-s3
|
||||
--with github.com/hslatman/caddy-crowdsec-bouncer/http@v0.10.0
|
||||
--with github.com/hslatman/caddy-crowdsec-bouncer/appsec@v0.10.0
|
||||
environment:
|
||||
GOCACHE: "/tmp/.gocache"
|
||||
PATH: "/usr/local/go/bin:{{ ansible_env.PATH }}"
|
||||
register: xcaddy_build
|
||||
notify: Reload Caddy
|
||||
|
||||
# Compatibility: Allow binding to 80/443 without root
|
||||
- name: Set capabilities for Caddy
|
||||
command: setcap cap_net_bind_service=+ep /usr/bin/caddy
|
||||
when: caddy_download.changed or (xcaddy_build is defined and xcaddy_build.changed)
|
||||
|
||||
- name: Create Caddy config directory
|
||||
file:
|
||||
path: /etc/caddy
|
||||
state: directory
|
||||
owner: caddy
|
||||
group: caddy
|
||||
mode: '0755'
|
||||
|
||||
- name: Ensure Caddy log directory exists
|
||||
file:
|
||||
path: /var/log/caddy
|
||||
state: directory
|
||||
owner: caddy
|
||||
group: caddy
|
||||
mode: '0755'
|
||||
|
||||
- name: Configure Caddyfile
|
||||
template:
|
||||
src: Caddyfile.j2
|
||||
dest: /etc/caddy/Caddyfile
|
||||
owner: caddy
|
||||
group: caddy
|
||||
mode: '0644'
|
||||
notify: Reload Caddy
|
||||
|
||||
- name: Install Systemd Service
|
||||
template:
|
||||
src: caddy.service.j2
|
||||
dest: /etc/systemd/system/caddy.service
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0644'
|
||||
notify: Reload Caddy
|
||||
|
||||
- name: Setup Caddy Dashboard Generator
|
||||
template:
|
||||
src: generate_dashboard.py.j2
|
||||
dest: /usr/local/bin/generate_dashboard.py
|
||||
mode: '0755'
|
||||
|
||||
- name: Setup Dashboard Service
|
||||
template:
|
||||
src: caddy-dashboard.service.j2
|
||||
dest: /etc/systemd/system/caddy-dashboard.service
|
||||
mode: '0644'
|
||||
|
||||
- name: Setup Dashboard Timer
|
||||
template:
|
||||
src: caddy-dashboard.timer.j2
|
||||
dest: /etc/systemd/system/caddy-dashboard.timer
|
||||
mode: '0644'
|
||||
|
||||
- name: Enable Dashboard Timer
|
||||
systemd:
|
||||
name: caddy-dashboard.timer
|
||||
state: started
|
||||
enabled: yes
|
||||
daemon_reload: yes
|
||||
|
||||
- name: Enable and Start Caddy Service
|
||||
systemd:
|
||||
name: caddy
|
||||
state: started
|
||||
enabled: yes
|
||||
daemon_reload: yes
|
||||
|
||||
- name: Configure Keepalived
|
||||
template:
|
||||
src: keepalived.conf.j2
|
||||
dest: /etc/keepalived/keepalived.conf
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0644'
|
||||
notify: Restart Keepalived
|
||||
vars:
|
||||
keepalived_vip: "{{ caddy_ha_vip }}"
|
||||
keepalived_auth_pass: "{{ caddy_ha_keepalived_auth_pass }}"
|
||||
@@ -1,395 +0,0 @@
|
||||
# The Caddyfile is an easy way to configure your Caddy web server.
|
||||
#
|
||||
# Unless the file starts with a global options block, the first
|
||||
# uncommented line is always the address of your site.
|
||||
#
|
||||
# To use your own domain name (with automatic HTTPS), first make
|
||||
# sure your domain's A/AAAA DNS records are properly pointed to
|
||||
# this machine's public IP, then replace ":80" below with your
|
||||
# domain name.
|
||||
{
|
||||
# BEGIN ANSIBLE MANAGED BLOCK
|
||||
# Admin API for metrics - listen on all interfaces for Prometheus scraping
|
||||
admin :2019
|
||||
|
||||
servers {
|
||||
trusted_proxies static 127.0.0.1 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16
|
||||
metrics
|
||||
}
|
||||
|
||||
# Layer4 TCP proxy — Gitea SSH on port 2222
|
||||
layer4 {
|
||||
:2222 {
|
||||
route {
|
||||
proxy gitea.srv.renner.uno:22
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{% if crowdsec_enabled | default(true) and crowdsec_caddy_api_key is defined %}
|
||||
# CrowdSec configuration
|
||||
crowdsec {
|
||||
api_url {{ crowdsec_lapi_url }}
|
||||
api_key {{ crowdsec_caddy_api_key }}
|
||||
ticker_interval {{ crowdsec_ticker_interval }}
|
||||
{% if crowdsec_appsec_url is defined %}
|
||||
appsec_url {{ crowdsec_appsec_url }}
|
||||
{% endif %}
|
||||
}
|
||||
|
||||
# Log configuration for CrowdSec analysis
|
||||
log {
|
||||
output file /var/log/caddy/access.log {
|
||||
roll_size 30MiB
|
||||
roll_keep 5
|
||||
}
|
||||
}
|
||||
{% endif %}
|
||||
# END ANSIBLE MANAGED BLOCK
|
||||
email tls@renner.uno
|
||||
# acme_dns cloudflare {
|
||||
# zone_token 8NcEXu8fSEv-gRVnh1kdL_sCh0cRCcUnRLgPGTA0
|
||||
# api_token 9Oncv6wU-R85y0FmgPnMQHhrANLiO2osRMNdRNeJ
|
||||
# }
|
||||
|
||||
default_sni caddy.renner.uno
|
||||
|
||||
}
|
||||
|
||||
# BEGIN ANSIBLE MANAGED BLOCK: Error Page Snippet
|
||||
(custom_error) {
|
||||
handle_errors {
|
||||
rewrite * /custom_error.html
|
||||
file_server {
|
||||
root /var/www/html/errors
|
||||
}
|
||||
}
|
||||
}
|
||||
# END ANSIBLE MANAGED BLOCK: Error Page Snippet
|
||||
|
||||
{% if crowdsec_enabled | default(true) and crowdsec_caddy_api_key is defined %}
|
||||
# BEGIN ANSIBLE MANAGED BLOCK: CrowdSec Snippet
|
||||
(crowdsec_protect) {
|
||||
crowdsec
|
||||
{% if crowdsec_appsec_url is defined %}
|
||||
appsec
|
||||
{% endif %}
|
||||
}
|
||||
# END ANSIBLE MANAGED BLOCK: CrowdSec Snippet
|
||||
{% endif %}
|
||||
|
||||
(cloudflare) {
|
||||
tls {
|
||||
dns cloudflare {
|
||||
zone_token 8NcEXu8fSEv-gRVnh1kdL_sCh0cRCcUnRLgPGTA0
|
||||
api_token 9Oncv6wU-R85y0FmgPnMQHhrANLiO2osRMNdRNeJ
|
||||
}
|
||||
resolvers 1.1.1.1
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
(loadbalancer) {
|
||||
lb_policy ip_hash
|
||||
#fail_duration 30s
|
||||
health_interval 15s
|
||||
health_timeout 10s
|
||||
}
|
||||
|
||||
|
||||
caddy.nerdcave.eu {
|
||||
import cloudflare
|
||||
# Set this path to your site's directory.
|
||||
root * /usr/share/caddy
|
||||
|
||||
# Enable the static file server.
|
||||
file_server
|
||||
|
||||
# Another common task is to set up a reverse proxy:
|
||||
# reverse_proxy localhost:8080
|
||||
|
||||
# Or serve a PHP site through php-fpm:
|
||||
# php_fastcgi localhost:9000
|
||||
}
|
||||
|
||||
caddy.renner.uno {
|
||||
import cloudflare
|
||||
root * /var/www/caddy-dashboard
|
||||
file_server
|
||||
}
|
||||
|
||||
# Internal Dashboard Access (Port 80) for Monitoring Script
|
||||
:80 {
|
||||
root * /var/www/caddy-dashboard
|
||||
file_server
|
||||
}
|
||||
|
||||
pve.nerdcave.eu {
|
||||
import cloudflare
|
||||
# import cloudflare
|
||||
redir https://pve.renner.uno{uri} permanent
|
||||
}
|
||||
|
||||
pve.renner.uno {
|
||||
import cloudflare
|
||||
# import cloudflare
|
||||
#import authelia
|
||||
reverse_proxy {
|
||||
to https://jupiter.renner.uno:8006 https://neptune.renner.uno:8006 https://saturn.renner.uno:8006
|
||||
health_port 8006
|
||||
#dynamic srv srv+https://_pveweb._tcp.nerdcave.eu {
|
||||
# resolvers 10.2.40.12 10.2.0.11
|
||||
#}
|
||||
import loadbalancer
|
||||
}
|
||||
}
|
||||
|
||||
pbs.renner.uno {
|
||||
import cloudflare
|
||||
|
||||
reverse_proxy https://pbs.obg.renner.uno:8007
|
||||
}
|
||||
|
||||
home.nerdcave.eu {
|
||||
import cloudflare
|
||||
log
|
||||
# import cloudflare
|
||||
redir https://home.renner.uno{uri} permanent
|
||||
}
|
||||
|
||||
home.renner.uno {
|
||||
import cloudflare
|
||||
reverse_proxy http://10.2.30.11:8123 {
|
||||
flush_interval -1
|
||||
transport http {
|
||||
keepalive 30s
|
||||
keepalive_idle_conns 10
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
home.internal.renner.uno {
|
||||
import cloudflare
|
||||
reverse_proxy http://10.2.30.11:8123 {
|
||||
flush_interval -1
|
||||
transport http {
|
||||
keepalive 30s
|
||||
keepalive_idle_conns 10
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
photos.nerdcave.eu {
|
||||
import cloudflare
|
||||
redir https://photos.renner.uno{uri} permanent
|
||||
}
|
||||
|
||||
photos.renner.uno {
|
||||
import cloudflare
|
||||
# import cloudflare
|
||||
redir /mo/sharing/mGwOwC3ZW https://immich.renner.uno/share/i8gRTJ6Kj0IczirRP6y-BvKcq3zXUnSIEYdtWjlcRJvDaHjStUgBmdKYeU58xZkzTWE
|
||||
redir /mo/sharing/4qry70DZG https://immich.renner.uno/share/i8gRTJ6Kj0IczirRP6y-BvKcq3zXUnSIEYdtWjlcRJvDaHjStUgBmdKYeU58xZkzTWE
|
||||
#reverse_proxy https://immich.renner.uno
|
||||
redir https://immich.renner.uno{uri} permanent
|
||||
}
|
||||
|
||||
file.nerdcave.eu {
|
||||
import cloudflare
|
||||
# import cloudflare
|
||||
reverse_proxy https://nas.renner.uno:7001
|
||||
}
|
||||
file.renner.uno {
|
||||
import cloudflare
|
||||
# import cloudflare
|
||||
reverse_proxy https://nas.renner.uno:7001
|
||||
}
|
||||
|
||||
docs.renner.uno {
|
||||
import cloudflare
|
||||
|
||||
@local {
|
||||
remote_ip 10.3.0.0/16
|
||||
#vars {tls_client_subject} ""
|
||||
}
|
||||
#@trusted {
|
||||
# vars {tls_client_issuer} "CN=Renner Home Intermediate CA,O=Renner Home"
|
||||
#}
|
||||
#import cloudflare
|
||||
handle @local {
|
||||
reverse_proxy http://paperless.srv.renner.uno:8000
|
||||
}
|
||||
handle {
|
||||
route {
|
||||
# always forward outpost path to actual outpost
|
||||
reverse_proxy /outpost.goauthentik.io/* http://authentik-app-1.srv.renner.uno:9000 http://authentik-app-2.srv.renner.uno:9000 {
|
||||
header_up Host {http.reverse_proxy.upstream.hostport}
|
||||
lb_policy round_robin
|
||||
}
|
||||
|
||||
# forward authentication to outpost
|
||||
forward_auth http://authentik-app-1.srv.renner.uno:9000 http://authentik-app-2.srv.renner.uno:9000 {
|
||||
uri /outpost.goauthentik.io/auth/caddy
|
||||
|
||||
# capitalization of the headers is important, otherwise they will be empty
|
||||
copy_headers X-Authentik-Username X-Authentik-Groups X-Authentik-Email X-Authentik-Name X-Authentik-Uid X-Authentik-Jwt X-Authentik-Meta-Jwks X-Authentik-Meta-Outpost X-Authentik-Meta-Provider X-Authentik-Meta-App X-Authentik-Meta-Version
|
||||
|
||||
# trust both authentik HA nodes
|
||||
trusted_proxies 10.2.40.0/24
|
||||
}
|
||||
reverse_proxy http://paperless.srv.renner.uno:8000
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
influx.renner.uno {
|
||||
import cloudflare
|
||||
# import cloudflare
|
||||
reverse_proxy http://influxdb.srv.renner.uno:8086
|
||||
}
|
||||
|
||||
dash.renner.uno {
|
||||
import cloudflare
|
||||
# import cloudflare
|
||||
reverse_proxy http://grafana.srv.renner.uno:3000
|
||||
}
|
||||
|
||||
|
||||
media.renner.uno {
|
||||
import cloudflare
|
||||
# import cloudflare
|
||||
#reverse_proxy http://10.2.40.15:8096
|
||||
reverse_proxy http://nas.renner.uno:8096
|
||||
}
|
||||
|
||||
git.renner.uno {
|
||||
import cloudflare
|
||||
# import cloudflare
|
||||
reverse_proxy http://gitea.srv.renner.uno:3000
|
||||
}
|
||||
|
||||
auth.renner.uno {
|
||||
import cloudflare
|
||||
reverse_proxy http://authentik-app-1.srv.renner.uno:9000 http://authentik-app-2.srv.renner.uno:9000 {
|
||||
lb_policy round_robin
|
||||
health_uri /-/health/live/
|
||||
health_interval 10s
|
||||
}
|
||||
}
|
||||
|
||||
audiobooks.renner.uno {
|
||||
import cloudflare
|
||||
reverse_proxy http://audiobookshelf.srv.renner.uno:13378
|
||||
}
|
||||
|
||||
wazuh.renner.uno {
|
||||
import cloudflare
|
||||
log {
|
||||
level info
|
||||
}
|
||||
reverse_proxy https://wazuh.srv.renner.uno:443 {
|
||||
transport http {
|
||||
tls_insecure_skip_verify
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Refer to the Caddy docs for more information:
|
||||
# https://caddyserver.com/docs/caddyfile
|
||||
# BEGIN ANSIBLE MANAGED BLOCK homeassistant-dev
|
||||
homeassistant-dev.renner.uno {
|
||||
import cloudflare
|
||||
import custom_error
|
||||
reverse_proxy http://10.2.40.121:8123
|
||||
}
|
||||
# END ANSIBLE MANAGED BLOCK homeassistant-dev
|
||||
|
||||
# BEGIN ANSIBLE MANAGED BLOCK GITEA
|
||||
gitea.renner.uno {
|
||||
reverse_proxy gitea.srv.renner.uno:3000
|
||||
import cloudflare
|
||||
}
|
||||
# END ANSIBLE MANAGED BLOCK GITEA
|
||||
|
||||
# BEGIN ANSIBLE MANAGED BLOCK IMMICH
|
||||
immich.renner.uno {
|
||||
import cloudflare
|
||||
import custom_error
|
||||
reverse_proxy http://immich.srv.renner.uno:2283
|
||||
}
|
||||
# END ANSIBLE MANAGED BLOCK IMMICH
|
||||
|
||||
ical-proxy.renner.uno {
|
||||
import cloudflare
|
||||
reverse_proxy http://ical-proxy.srv.renner.uno:8080
|
||||
}
|
||||
|
||||
|
||||
# BEGIN ANSIBLE MANAGED BLOCK MUSIC-ASSISTANT
|
||||
music.renner.uno {
|
||||
import cloudflare
|
||||
import custom_error
|
||||
reverse_proxy http://music-assistant.iot.renner.uno:8095
|
||||
}
|
||||
# END ANSIBLE MANAGED BLOCK MUSIC-ASSISTANT
|
||||
|
||||
# BEGIN ANSIBLE MANAGED BLOCK WIKI
|
||||
wiki.renner.uno {
|
||||
import cloudflare
|
||||
import custom_error
|
||||
reverse_proxy http://wiki.srv.renner.uno:80
|
||||
}
|
||||
# END ANSIBLE MANAGED BLOCK WIKI
|
||||
|
||||
|
||||
# BEGIN ANSIBLE MANAGED BLOCK WOODPECKER
|
||||
ci.renner.uno {
|
||||
import cloudflare
|
||||
import custom_error
|
||||
reverse_proxy http://woodpecker.srv.renner.uno:8000
|
||||
}
|
||||
# END ANSIBLE MANAGED BLOCK WOODPECKER
|
||||
|
||||
# BEGIN ANSIBLE MANAGED BLOCK OWNCLOUD
|
||||
owncloud.renner.uno {
|
||||
import cloudflare
|
||||
import custom_error
|
||||
reverse_proxy http://owncloud.srv.renner.uno:9200
|
||||
}
|
||||
# END ANSIBLE MANAGED BLOCK OWNCLOUD
|
||||
|
||||
# BEGIN ANSIBLE MANAGED BLOCK ONLYOFFICE
|
||||
office.renner.uno {
|
||||
import cloudflare
|
||||
import custom_error
|
||||
reverse_proxy http://owncloud.srv.renner.uno:9980 {
|
||||
header_down X-Frame-Options "ALLOWALL"
|
||||
header_down -Content-Security-Policy
|
||||
}
|
||||
}
|
||||
# END ANSIBLE MANAGED BLOCK ONLYOFFICE
|
||||
|
||||
# BEGIN ANSIBLE MANAGED BLOCK GATUS
|
||||
status.renner.uno {
|
||||
import cloudflare
|
||||
import custom_error
|
||||
reverse_proxy http://gatus.srv.renner.uno:8080
|
||||
}
|
||||
# END ANSIBLE MANAGED BLOCK GATUS
|
||||
|
||||
# BEGIN ANSIBLE MANAGED BLOCK KASM
|
||||
kasm.renner.uno {
|
||||
import cloudflare
|
||||
import custom_error
|
||||
reverse_proxy https://kasm.srv.renner.uno:443 {
|
||||
transport http {
|
||||
tls_insecure_skip_verify
|
||||
keepalive 30s
|
||||
}
|
||||
flush_interval -1
|
||||
}
|
||||
}
|
||||
# END ANSIBLE MANAGED BLOCK KASM
|
||||
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
[Unit]
|
||||
Description=Generate Caddy HA Dashboard
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/bin/python3 /usr/local/bin/generate_dashboard.py
|
||||
User=root
|
||||
Group=root
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -1,9 +0,0 @@
|
||||
[Unit]
|
||||
Description=Run Caddy Dashboard Generator every 5 seconds
|
||||
|
||||
[Timer]
|
||||
OnBootSec=10s
|
||||
OnUnitActiveSec=5s
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
@@ -1,35 +0,0 @@
|
||||
# caddy.service
|
||||
#
|
||||
# For using Caddy with a config file.
|
||||
#
|
||||
# Make sure the ExecStart and ExecReload commands are correct
|
||||
# for your installation.
|
||||
#
|
||||
# See https://caddyserver.com/docs/install for instructions.
|
||||
#
|
||||
# WARNING: This service does not use the --resume flag, so if you
|
||||
# use the API to make changes, they will be overwritten by the
|
||||
# Caddyfile next time the service is restarted. If you intend to
|
||||
# use Caddy's API to configure it, add the --resume flag to the
|
||||
# `caddy run` command or use the caddy-api.service file instead.
|
||||
|
||||
[Unit]
|
||||
Description=Caddy
|
||||
Documentation=https://caddyserver.com/docs/
|
||||
After=network.target network-online.target
|
||||
Requires=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=notify
|
||||
User=caddy
|
||||
Group=caddy
|
||||
ExecStart=/usr/bin/caddy run --environ --config /etc/caddy/Caddyfile
|
||||
ExecReload=/usr/bin/caddy reload --config /etc/caddy/Caddyfile --force
|
||||
TimeoutStopSec=5s
|
||||
LimitNOFILE=1048576
|
||||
PrivateTmp=true
|
||||
ProtectSystem=full
|
||||
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -1,199 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import subprocess
|
||||
import socket
|
||||
import datetime
|
||||
import os
|
||||
import sys
|
||||
import ssl
|
||||
import urllib.request
|
||||
|
||||
# Constants
|
||||
HOSTNAME = socket.gethostname()
|
||||
VIP_IP = "10.2.45.100" # Adjust if your VIP is different
|
||||
OUTPUT_FILE = "/var/www/caddy-dashboard/index.html"
|
||||
NODES = [
|
||||
{% for host in groups['proxmox_caddy_ha'] %}
|
||||
{
|
||||
"name": "{{ hostvars[host].ansible_host }} ({{ host }})",
|
||||
"host": "{{ hostvars[host].ansible_host }}"
|
||||
},
|
||||
{% endfor %}
|
||||
]
|
||||
|
||||
{% raw %}
|
||||
def check_remote_node_status(host):
|
||||
"""Checks the status of a remote node by fetching its dashboard."""
|
||||
try:
|
||||
# Use HTTP on port 80 to bypass TLS/SNI issues
|
||||
url = f"http://{host}"
|
||||
req = urllib.request.Request(url)
|
||||
|
||||
with urllib.request.urlopen(req, timeout=1) as response:
|
||||
html = response.read().decode('utf-8')
|
||||
if "MASTER (Active Node)" in html:
|
||||
return "Online (MASTER)"
|
||||
elif "BACKUP (Standby Node)" in html:
|
||||
return "Online (BACKUP)"
|
||||
else:
|
||||
return "Online"
|
||||
except Exception:
|
||||
return "Offline"
|
||||
|
||||
def get_service_status(service):
|
||||
"""Checks if a systemd service is active."""
|
||||
try:
|
||||
subprocess.check_call(["systemctl", "is-active", "--quiet", service])
|
||||
return "Active"
|
||||
except subprocess.CalledProcessError:
|
||||
return "Inactive"
|
||||
except FileNotFoundError:
|
||||
return "Not Installed"
|
||||
|
||||
def get_keepalived_role():
|
||||
"""Checks if the VIP is present on any interface."""
|
||||
try:
|
||||
output = subprocess.check_output(["ip", "addr", "show"]).decode()
|
||||
if VIP_IP in output:
|
||||
return "MASTER (Active Node)"
|
||||
return "BACKUP (Standby Node)"
|
||||
except Exception:
|
||||
return "Unknown"
|
||||
|
||||
def generate_html():
|
||||
caddy_status = get_service_status("caddy")
|
||||
keepalived_status = get_service_status("keepalived")
|
||||
node_role = get_keepalived_role()
|
||||
last_updated = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
# Generate Node Status List
|
||||
nodes_html = ""
|
||||
for node in NODES:
|
||||
status = check_remote_node_status(node["host"])
|
||||
|
||||
status_class = "inactive"
|
||||
if "Online" in status:
|
||||
status_class = "active"
|
||||
if "MASTER" in status:
|
||||
status_class = "master"
|
||||
elif "BACKUP" in status:
|
||||
status_class = "backup"
|
||||
|
||||
nodes_html += f"""
|
||||
<div class="item">
|
||||
<span class="label">{node['name']}</span>
|
||||
<span class="status {status_class}">{status}</span>
|
||||
</div>
|
||||
"""
|
||||
|
||||
# CSS classes based on status
|
||||
role_class = "master" if "MASTER" in node_role else "backup"
|
||||
caddy_class = "active" if caddy_status == "Active" else "inactive"
|
||||
keepalived_class = "active" if keepalived_status == "Active" else "inactive"
|
||||
|
||||
html_content = f"""<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Caddy HA Status - {HOSTNAME}</title>
|
||||
<meta http-equiv="refresh" content="5">
|
||||
<style>
|
||||
body {{
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
background-color: #1a1a1a;
|
||||
color: #e0e0e0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
}}
|
||||
.card {{
|
||||
background-color: #2d2d2d;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 20px rgba(0,0,0,0.4);
|
||||
padding: 2rem;
|
||||
width: 90%;
|
||||
max-width: 450px;
|
||||
}}
|
||||
h1 {{
|
||||
margin-top: 0;
|
||||
border-bottom: 2px solid #404040;
|
||||
padding-bottom: 1rem;
|
||||
text-align: center;
|
||||
font-size: 1.8rem;
|
||||
color: #fff;
|
||||
}}
|
||||
.item {{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin: 1.2rem 0;
|
||||
font-size: 1.1rem;
|
||||
}}
|
||||
.label {{
|
||||
color: #aaa;
|
||||
font-weight: 500;
|
||||
}}
|
||||
.status {{
|
||||
font-weight: 700;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 6px;
|
||||
}}
|
||||
.active {{ color: #4caf50; background: rgba(76, 175, 80, 0.1); }}
|
||||
.inactive {{ color: #f44336; background: rgba(244, 67, 54, 0.1); }}
|
||||
.master {{ color: #4caf50; background: rgba(76, 175, 80, 0.1); border: 1px solid #4caf50; }}
|
||||
.backup {{ color: #ff9800; background: rgba(255, 152, 0, 0.1); border: 1px solid #ff9800; }}
|
||||
.footer {{
|
||||
margin-top: 2rem;
|
||||
text-align: right;
|
||||
font-size: 0.8rem;
|
||||
color: #666;
|
||||
border-top: 1px solid #404040;
|
||||
padding-top: 1rem;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<h1>{HOSTNAME}</h1>
|
||||
|
||||
<div class="item">
|
||||
<span class="label">HA Role</span>
|
||||
<span class="status {role_class}">{node_role}</span>
|
||||
</div>
|
||||
|
||||
<div class="item">
|
||||
<span class="label">Caddy Service</span>
|
||||
<span class="status {caddy_class}">{caddy_status}</span>
|
||||
</div>
|
||||
|
||||
<div class="item">
|
||||
<span class="label">Keepalived</span>
|
||||
<span class="status {keepalived_class}">{keepalived_status}</span>
|
||||
</div>
|
||||
|
||||
<h2 style="font-size: 1.2rem; margin-top: 2rem; border-bottom: 1px solid #404040; padding-bottom: 0.5rem; color: #fff; margin-bottom: 1rem;">Cluster Nodes</h2>
|
||||
{nodes_html}
|
||||
|
||||
<div class="footer">
|
||||
Updated: {last_updated}
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
# Ensure directory exists
|
||||
os.makedirs(os.path.dirname(OUTPUT_FILE), exist_ok=True)
|
||||
|
||||
with open(OUTPUT_FILE, "w") as f:
|
||||
f.write(html_content)
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
generate_html()
|
||||
except Exception as e:
|
||||
print(f"Error generating dashboard: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
{% endraw %}
|
||||
@@ -1,23 +0,0 @@
|
||||
vrrp_script chk_caddy {
|
||||
script "/usr/bin/pgrep caddy"
|
||||
interval 2
|
||||
weight -50
|
||||
}
|
||||
|
||||
vrrp_instance VI_1 {
|
||||
state {{ keepalived_state }}
|
||||
interface {{ ansible_facts["default_ipv4"].interface | default('ens18') }}
|
||||
virtual_router_id 51
|
||||
priority {{ keepalived_priority }}
|
||||
advert_int 1
|
||||
authentication {
|
||||
auth_type PASS
|
||||
auth_pass {{ keepalived_auth_pass | default('42secret') }}
|
||||
}
|
||||
virtual_ipaddress {
|
||||
{{ keepalived_vip }}
|
||||
}
|
||||
track_script {
|
||||
chk_caddy
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
---
|
||||
cloudflared_token: "{{ vault_cloudflared_token }}"
|
||||
@@ -1,43 +0,0 @@
|
||||
---
|
||||
- name: Install dependencies
|
||||
apt:
|
||||
name:
|
||||
- curl
|
||||
- lsb-release
|
||||
state: present
|
||||
update_cache: yes
|
||||
|
||||
- name: Create keyring directory
|
||||
file:
|
||||
path: /usr/share/keyrings
|
||||
state: directory
|
||||
mode: '0755'
|
||||
|
||||
- name: Add Cloudflare GPG key
|
||||
get_url:
|
||||
url: https://pkg.cloudflare.com/cloudflare-main.gpg
|
||||
dest: /usr/share/keyrings/cloudflare-main.gpg
|
||||
mode: '0644'
|
||||
|
||||
- name: Add Cloudflare repository
|
||||
apt_repository:
|
||||
repo: "deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared {{ ansible_distribution_release | default('bookworm') }} main"
|
||||
filename: cloudflare
|
||||
state: present
|
||||
|
||||
- name: Install cloudflared
|
||||
apt:
|
||||
name: cloudflared
|
||||
state: present
|
||||
update_cache: yes
|
||||
|
||||
- name: Install cloudflared service
|
||||
command: cloudflared service install {{ cloudflared_token }}
|
||||
args:
|
||||
creates: /etc/systemd/system/cloudflared.service
|
||||
|
||||
- name: Enable and start cloudflared service
|
||||
service:
|
||||
name: cloudflared
|
||||
state: started
|
||||
enabled: yes
|
||||
@@ -1,12 +0,0 @@
|
||||
---
|
||||
# NetBird Gateway Configuration
|
||||
netbird_server_url: "https://netbird.renner.uno"
|
||||
netbird_setup_key: "" # Set in vault: netbird_gateway_setup_key
|
||||
|
||||
# Routes to advertise to the NetBird network
|
||||
netbird_routes:
|
||||
- "10.2.40.0/24" # v40srv - services network
|
||||
- "10.2.45.0/24" # v42ing - ingress network
|
||||
|
||||
# DNS settings for split DNS
|
||||
netbird_dns_server: "10.2.40.53" # Proxmox DNS VIP
|
||||
@@ -1,12 +0,0 @@
|
||||
---
|
||||
- name: Save iptables rules
|
||||
ansible.builtin.shell: |
|
||||
iptables-save > /etc/iptables/rules.v4 || true
|
||||
ip6tables-save > /etc/iptables/rules.v6 || true
|
||||
args:
|
||||
creates: /etc/iptables/rules.v4
|
||||
|
||||
- name: Restart netbird
|
||||
ansible.builtin.service:
|
||||
name: netbird
|
||||
state: restarted
|
||||
@@ -1,82 +0,0 @@
|
||||
---
|
||||
- name: Install required packages
|
||||
ansible.builtin.apt:
|
||||
name:
|
||||
- curl
|
||||
- ca-certificates
|
||||
- iptables
|
||||
- gnupg
|
||||
state: present
|
||||
update_cache: true
|
||||
|
||||
- name: Enable IP forwarding
|
||||
ansible.posix.sysctl:
|
||||
name: "{{ item }}"
|
||||
value: "1"
|
||||
sysctl_set: true
|
||||
state: present
|
||||
reload: true
|
||||
loop:
|
||||
- net.ipv4.ip_forward
|
||||
- net.ipv6.conf.all.forwarding
|
||||
|
||||
- name: Add NetBird repository key
|
||||
ansible.builtin.shell:
|
||||
cmd: |
|
||||
curl -sSL https://pkgs.netbird.io/debian/public.key | gpg --dearmor -o /usr/share/keyrings/netbird-archive-keyring.gpg
|
||||
creates: /usr/share/keyrings/netbird-archive-keyring.gpg
|
||||
|
||||
- name: Add NetBird repository
|
||||
ansible.builtin.apt_repository:
|
||||
repo: "deb [signed-by=/usr/share/keyrings/netbird-archive-keyring.gpg] https://pkgs.netbird.io/debian stable main"
|
||||
state: present
|
||||
filename: netbird
|
||||
|
||||
- name: Install NetBird client
|
||||
ansible.builtin.apt:
|
||||
name: netbird
|
||||
state: present
|
||||
update_cache: true
|
||||
|
||||
- name: Check if NetBird is already connected
|
||||
ansible.builtin.command: netbird status
|
||||
register: netbird_status
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Connect NetBird to management server
|
||||
ansible.builtin.command: >
|
||||
netbird up
|
||||
--management-url {{ netbird_server_url }}
|
||||
--setup-key {{ netbird_gateway_setup_key }}
|
||||
when: "'Connected' not in netbird_status.stdout"
|
||||
register: netbird_connect
|
||||
changed_when: netbird_connect.rc == 0
|
||||
|
||||
- name: Enable NetBird service
|
||||
ansible.builtin.service:
|
||||
name: netbird
|
||||
state: started
|
||||
enabled: true
|
||||
|
||||
- name: Configure iptables for NAT (masquerade)
|
||||
ansible.builtin.iptables:
|
||||
table: nat
|
||||
chain: POSTROUTING
|
||||
source: "100.64.0.0/10"
|
||||
jump: MASQUERADE
|
||||
comment: "NetBird NAT for VPN clients"
|
||||
notify: Save iptables rules
|
||||
|
||||
- name: Display gateway status
|
||||
ansible.builtin.debug:
|
||||
msg: |
|
||||
NetBird gateway configured.
|
||||
Server: {{ netbird_server_url }}
|
||||
Routes advertised: {{ netbird_routes | join(', ') }}
|
||||
|
||||
To add routes in NetBird management:
|
||||
1. Go to {{ netbird_server_url }}
|
||||
2. Navigate to Network Routes
|
||||
3. Add routes for: {{ netbird_routes | join(', ') }}
|
||||
4. Select this peer as the routing peer
|
||||
@@ -92,4 +92,5 @@
|
||||
To create a setup key:
|
||||
1. Log into https://{{ netbird_server_host }} with your Authentik credentials
|
||||
2. Go to Setup Keys > Create Setup Key
|
||||
3. Use the key with the netbird-gateway role
|
||||
3. Use the key with the netbird-ingress DaemonSet
|
||||
(kubernetes/infrastructure/netbird-ingress) or the netbird-proxy role
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
FROM golang:1.26
|
||||
|
||||
ARG XCADDY_VERSION=v0.4.5
|
||||
|
||||
RUN go install github.com/caddyserver/xcaddy/cmd/xcaddy@${XCADDY_VERSION}
|
||||
@@ -28,7 +28,6 @@ locals {
|
||||
# LXCs
|
||||
[
|
||||
"114", # gitea
|
||||
"117", # cloudflared
|
||||
"118", # proxmox-dns-1 (jupiter)
|
||||
"121", # ical-proxy
|
||||
"130", # authentik-app-1
|
||||
@@ -42,10 +41,6 @@ locals {
|
||||
"227", # prometheus
|
||||
"229", # music-assistant
|
||||
"231", # woodpecker
|
||||
"232", # caddy-1
|
||||
"233", # caddy-2
|
||||
"234", # caddy-3
|
||||
"260", # netbird-gateway
|
||||
"318", # proxmox-dns-3 (neptune)
|
||||
],
|
||||
# Woodpecker CI agents (VMIDs 910..)
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
# Example: Manage a Cloudflare Tunnel
|
||||
# 1. Create the Tunnel Resource
|
||||
# resource "cloudflare_tunnel" "homelab_tunnel" {
|
||||
# account_id = var.cloudflare_account_id
|
||||
# name = "homelab-tunnel"
|
||||
# secret = base64sha256(random_password.tunnel_secret.result)
|
||||
# }
|
||||
#
|
||||
# 2. Generate a random secret for the tunnel
|
||||
# resource "random_password" "tunnel_secret" {
|
||||
# length = 32
|
||||
# special = false
|
||||
# }
|
||||
#
|
||||
# 3. Configure the Tunnel (Ingress Rules)
|
||||
# resource "cloudflare_tunnel_config" "homelab_config" {
|
||||
# account_id = var.cloudflare_account_id
|
||||
# tunnel_id = cloudflare_tunnel.homelab_tunnel.id
|
||||
#
|
||||
# config {
|
||||
# ingress_rule {
|
||||
# hostname = "gitea.renner.uno"
|
||||
# service = "http://10.2.40.114:3000"
|
||||
# origin_request {
|
||||
# connect_timeout = "1m"
|
||||
# }
|
||||
# }
|
||||
#
|
||||
# # Catch-all rule (Required)
|
||||
# ingress_rule {
|
||||
# service = "http_status:404"
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
#
|
||||
# 4. Create DNS Records pointing to the Tunnel
|
||||
# resource "cloudflare_record" "gitea" {
|
||||
# zone_id = "your_zone_id_here"
|
||||
# name = "gitea"
|
||||
# value = "${cloudflare_tunnel.homelab_tunnel.id}.cfargotunnel.com"
|
||||
# type = "CNAME"
|
||||
# proxied = true
|
||||
# }
|
||||
@@ -1,106 +0,0 @@
|
||||
moved {
|
||||
from = proxmox_lxc.caddy_srv_renner_uno
|
||||
to = module.caddy_srv_renner_uno.proxmox_lxc.this
|
||||
}
|
||||
|
||||
module "caddy_srv_renner_uno" {
|
||||
source = "./modules/proxmox_lxc"
|
||||
|
||||
target_node = "neptune"
|
||||
hostname = "caddy.dmz.renner.uno"
|
||||
vmid = 232
|
||||
memory = 1024
|
||||
cores = 1
|
||||
|
||||
unprivileged = true
|
||||
cpuunits = 1024
|
||||
tags = "caddy_ha,netbird_client"
|
||||
|
||||
hagroup = "olymp"
|
||||
hastate = "stopped"
|
||||
|
||||
storage = "local-zfs"
|
||||
disk_size = "8G"
|
||||
network_bridge = "v42ing"
|
||||
# ip_address = "10.2.45.101"
|
||||
mac_address = "bc:24:11:de:8f:a4"
|
||||
|
||||
ssh_public_keys = local.common_ssh_keys
|
||||
|
||||
start = false
|
||||
onboot = false
|
||||
|
||||
# All Caddy ingress hostnames retired:
|
||||
# pve/home/auth/dash dropped in the prior commit (5eb8ad5).
|
||||
# file.renner.uno: now served by apps/file (ExternalName -> nas:7001).
|
||||
# dir.renner.uno + caddy.renner.uno: stale (no live backend).
|
||||
ingress_cnames = []
|
||||
}
|
||||
|
||||
moved {
|
||||
from = proxmox_lxc.caddy2_srv_renner_uno
|
||||
to = module.caddy2_srv_renner_uno.proxmox_lxc.this
|
||||
}
|
||||
|
||||
module "caddy2_srv_renner_uno" {
|
||||
source = "./modules/proxmox_lxc"
|
||||
|
||||
target_node = "saturn"
|
||||
hostname = "caddy2.dmz.renner.uno"
|
||||
vmid = 233
|
||||
memory = 1024
|
||||
cores = 1
|
||||
|
||||
unprivileged = true
|
||||
cpuunits = 1024
|
||||
tags = "caddy_ha,netbird_client"
|
||||
|
||||
hagroup = "olymp"
|
||||
hastate = "stopped"
|
||||
|
||||
storage = "local-zfs"
|
||||
disk_size = "8G"
|
||||
network_bridge = "v42ing"
|
||||
# ip_address = "10.2.45.103"
|
||||
mac_address = "bc:24:11:85:43:9c"
|
||||
|
||||
ssh_public_keys = local.common_ssh_keys
|
||||
|
||||
start = false
|
||||
onboot = false
|
||||
}
|
||||
|
||||
module "caddy3_srv_renner_uno" {
|
||||
source = "./modules/proxmox_lxc"
|
||||
|
||||
target_node = "jupiter"
|
||||
hostname = "caddy3.dmz.renner.uno"
|
||||
vmid = 234
|
||||
memory = 1024
|
||||
cores = 1
|
||||
|
||||
unprivileged = true
|
||||
cpuunits = 1024
|
||||
tags = "caddy_ha,netbird_client"
|
||||
|
||||
hagroup = "olymp"
|
||||
hastate = "stopped"
|
||||
|
||||
storage = "local-zfs"
|
||||
disk_size = "8G"
|
||||
network_bridge = "v42ing"
|
||||
|
||||
ssh_public_keys = local.common_ssh_keys
|
||||
|
||||
start = false
|
||||
onboot = false
|
||||
}
|
||||
|
||||
|
||||
# caddy_vip A record removed: no CNAMEs point at ingress.renner.uno
|
||||
# anymore (caddy module's ingress_cnames is empty + every per-LXC/VM
|
||||
# ingress_cnames was emptied in the cleanup pass). Keepalived-managed
|
||||
# 10.2.45.100 dies with the Caddy LXCs anyway.
|
||||
|
||||
# Old authentik.renner.uno CNAME removed - migrated to auth.renner.uno with HA setup
|
||||
# auth.renner.uno is handled via Caddy reverse proxy to authentik-app-1/2.srv.renner.uno
|
||||
@@ -1,34 +0,0 @@
|
||||
moved {
|
||||
from = proxmox_lxc.lxc["117"]
|
||||
to = proxmox_lxc.cloudflared_lxc
|
||||
}
|
||||
|
||||
module "cloudflared_lxc" {
|
||||
source = "./modules/proxmox_lxc"
|
||||
|
||||
target_node = "saturn"
|
||||
hostname = "cloudflared.srv.renner.uno"
|
||||
vmid = 117
|
||||
memory = 1024
|
||||
cores = 1
|
||||
|
||||
unprivileged = false
|
||||
cpuunits = 1024
|
||||
|
||||
hagroup = "olymp"
|
||||
hastate = "started"
|
||||
|
||||
storage = "local-zfs"
|
||||
disk_size = "4G"
|
||||
network_bridge = "v42ing"
|
||||
|
||||
start = false
|
||||
onboot = false
|
||||
|
||||
tags = "cloudflared"
|
||||
}
|
||||
|
||||
moved {
|
||||
from = proxmox_lxc.cloudflared_lxc
|
||||
to = module.cloudflared_lxc.proxmox_lxc.this
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
module "netbird_gateway" {
|
||||
source = "./modules/proxmox_lxc"
|
||||
|
||||
target_node = "jupiter"
|
||||
hostname = "netbird-gateway.srv.renner.uno"
|
||||
vmid = 260
|
||||
memory = 512
|
||||
cores = 1
|
||||
|
||||
unprivileged = true
|
||||
cpuunits = 1024
|
||||
|
||||
hagroup = "olymp"
|
||||
hastate = "stopped"
|
||||
|
||||
storage = "local-zfs"
|
||||
disk_size = "4G"
|
||||
network_bridge = "v40srv"
|
||||
|
||||
start = false
|
||||
onboot = false
|
||||
|
||||
tags = "netbird,gateway"
|
||||
|
||||
description = "NetBird gateway - routes VPN traffic to internal services"
|
||||
|
||||
ansible_playbook = "plays/install-netbird-gateway.yml"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
variable "cloudflare_api_token" {
|
||||
type = string
|
||||
description = "The Cloudflare API Token"
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "cloudflare_account_id" {
|
||||
type = string
|
||||
description = "The Cloudflare Account ID"
|
||||
sensitive = true
|
||||
}
|
||||
Reference in New Issue
Block a user