rework everything to be Ansible and get TLS working

This commit is contained in:
Josh Sandlin
2025-09-05 18:18:28 -04:00
parent 59fbd0ebbe
commit 6d1d0557bd
20 changed files with 475 additions and 206 deletions

View File

@@ -29,10 +29,7 @@
1. Doppler is installed and authenticated for use with the `ttl-sh` project
a. Have a valid `DOPPLER_TOKEN` from `ttl-sh` set
b. `echo $DOPPLER_TOKEN | doppler configure set token --scope /`
2. `./install.sh`
3. Successful run states:
a. `Container ttlsh-redis Started`
b. `Container ttlsh-hooks Started`
b. `Container ttlsh-reaper Started`
c. `Container ttlsh-registry Started`
d. `Container ttlsh-nginx Started`
2. `cd ansible`
3. `./ansible.sh`
4. Successful run indicates approximately the following:
a. `ok=42 changed=6 unreachable=0 failed=0 skipped=3`

12
ansible/ansible.cfg Normal file
View File

@@ -0,0 +1,12 @@
[defaults]
inventory = ./inventory/hosts
interpreter_python = auto_silent
retry_files_enabled = False
stdout_callback = default
bin_ansible_callbacks = True
[callback_default]
result_format = yaml
[ssh_connection]
pipelining = True

66
ansible/ansible.sh Executable file
View File

@@ -0,0 +1,66 @@
#!/usr/bin/env bash
set -euo pipefail
# Minimal wrapper to run Ansible with Cloudflare vars resolved via Doppler.
# Usage:
# bash ansible.sh
HERE="$(cd "$(dirname "$0")" && pwd)"
require_bin() {
command -v "$1" >/dev/null 2>&1 || {
echo "error: required command not found: $1" >&2
exit 1
}
}
require_env() {
local name=$1
if [[ -z "${!name:-}" ]]; then
echo "error: required env var not set: $name" >&2
exit 1
fi
}
get_secret() {
local key=$1
# Uses DOPPLER_TOKEN from environment; Doppler CLI will read it.
# --plain prints only the value with a trailing newline, strip it.
local val
if ! val=$(doppler secrets get "$key" --plain 2>/dev/null); then
echo "error: failed to fetch secret '$key' from Doppler" >&2
exit 1
fi
printf %s "$val"
}
main() {
require_bin ansible-playbook
require_bin doppler
require_env DOPPLER_TOKEN
# Always run from the ansible dir so relative paths match
cd "$HERE"
ansible-playbook \
-e "cloudflare_api_token=$(get_secret CF_API_TOKEN)" \
-e "cloudflare_zone_id=$(get_secret CF_ZONE_ID)" \
-e "cloudflare_email=$(get_secret LE_EMAIL)" \
-e "le_email=$(get_secret LE_EMAIL)" \
-e "gcloud_sa_email=$(get_secret GOOGLE_APPLICATION_CREDENTIALS_EMAIL)" \
-e "gcloud_sa_key_json=$(get_secret GCS_KEY_ENCODED)" \
-e "gcloud_registry_host=$(get_secret GOOGLE_CLOUD_ARTIFACT_REGISTRY_URI)" \
-e "gcs_key_encoded=$(get_secret GCS_KEY_ENCODED)" \
-e "hook_token=$(get_secret HOOK_TOKEN)" \
-e "hook_uri=$(get_secret HOOK_URI)" \
-e "redis_password=$(get_secret REDIS_PASSWORD)" \
-e "rediscloud_url=$(get_secret REDISCLOUD_URL)" \
-e "replreg_host=$(get_secret REPLREG_HOST)" \
-e "replreg_secret=$(get_secret REPLREG_SECRET)" \
-e "registry_url=$(get_secret REGISTRY_URL)" \
playbooks/site.yml
}
main "$@"

View File

@@ -0,0 +1,45 @@
---
# Domain to provision
domain_name: ttl.sh
# Compute zone name from domain (ttl.sh)
zone_name: "ttl.sh"
# Cloudflare details
cloudflare_zone_id: "_in_doppler"
cloudflare_api_token: "_in_doppler"
cloudflare_email: "_in_doppler"
cloudflare_proxied: false
# A record target
target_ip: 178.156.198.215
# Let's Encrypt contact email (using provided email)
le_email: "_in_doppler"
# Repo path (relative to repo root) for static assets; easy to change later
static_site_repo_path: static
# Google Cloud (gcloud) configuration
# Provide the service account email and base64-encoded JSON key to configure auth.
gcloud_sa_email: "_in_doppler"
gcloud_sa_key_json: "_in_doppler"
# Artifact Registry hostname to configure Docker for
gcloud_registry_host: "_in_doppler"
# Docker install settings
docker_users:
- root
# Application deployment
app_root: /opt/ttlsh
# Docker Compose .env
gcs_key_encoded: "_in_doppler"
hook_token: "_in_doppler"
hook_uri: "_in_doppler"
redis_password: "_in_doppler"
rediscloud_url: "_in_doppler"
replreg_host: "_in_doppler"
replreg_secret: "_in_doppler"
registry_url: "_in_doppler"

2
ansible/inventory/hosts Normal file
View File

@@ -0,0 +1,2 @@
[ttlsh]
178.156.198.215 ansible_user=root

285
ansible/playbooks/site.yml Normal file
View File

@@ -0,0 +1,285 @@
---
# Single play: run everything against the remote host
- name: Install and configure Nginx + Certbot (Cloudflare DNS-01)
hosts: ttlsh
become: true
vars:
site_dir: "/var/www/{{ domain_name }}/html"
cloudflare_credentials_path: /etc/letsencrypt/cloudflare.ini
# derive DNS record prefix, e.g. 'dev' for dev.ttl.sh
dns_record: "{{ '@' if domain_name == zone_name else domain_name[:-(zone_name|length + 1)] }}"
# repo-relative source for static assets
static_src_path: "{{ playbook_dir }}/../../{{ static_site_repo_path }}/"
pre_tasks: []
tasks:
- name: Ensure Cloudflare API token present
ansible.builtin.assert:
that: cloudflare_api_token is defined and (cloudflare_api_token | length) > 0
fail_msg: "cloudflare_api_token is missing."
- name: Ensure A record for {{ domain_name }} -> {{ target_ip }} (Cloudflare)
community.general.cloudflare_dns:
zone: "{{ zone_name }}"
record: "{{ dns_record }}"
type: A
value: "{{ target_ip }}"
proxied: "{{ cloudflare_proxied }}"
api_token: "{{ cloudflare_api_token }}"
- name: Install packages
ansible.builtin.apt:
name:
- nginx
- certbot
- python3-certbot-dns-cloudflare
update_cache: true
state: present
- name: Ensure nginx is enabled and started
ansible.builtin.service:
name: nginx
state: started
enabled: true
- name: Write Cloudflare credentials for Certbot
ansible.builtin.template:
src: ../templates/cloudflare.ini.j2
dest: "{{ cloudflare_credentials_path }}"
owner: root
group: root
mode: "0600"
- name: Check if certificate already exists
ansible.builtin.stat:
path: "/etc/letsencrypt/live/{{ domain_name }}/fullchain.pem"
register: cert_stat
- name: Obtain certificate using DNS-01 (Cloudflare)
ansible.builtin.command: >-
certbot certonly
--agree-tos
--email {{ le_email }}
--key-type ecdsa
--elliptic-curve secp384r1
--dns-cloudflare
--dns-cloudflare-credentials {{ cloudflare_credentials_path }}
--dns-cloudflare-propagation-seconds 60
-d {{ domain_name }}
--non-interactive
args:
creates: "/etc/letsencrypt/live/{{ domain_name }}/fullchain.pem"
when: not cert_stat.stat.exists
- name: Create web root
ansible.builtin.file:
path: "{{ site_dir }}"
state: directory
owner: www-data
group: www-data
mode: "0755"
recurse: true
- name: Deploy static assets from repo
ansible.builtin.copy:
src: "{{ static_src_path }}"
dest: "{{ site_dir }}/"
owner: www-data
group: www-data
mode: "0644"
directory_mode: "0755"
- name: Remove legacy test index.html if present
ansible.builtin.file:
path: "{{ site_dir }}/index.html"
state: absent
- name: Drop Nginx site config (TLS enabled)
ansible.builtin.template:
src: ../templates/nginx_site.conf.j2
dest: "/etc/nginx/sites-available/{{ domain_name }}"
owner: root
group: root
mode: "0644"
- name: Disable default site if present (available)
ansible.builtin.file:
path: /etc/nginx/sites-available/default
state: absent
- name: Disable default site if present (enabled)
ansible.builtin.file:
path: /etc/nginx/sites-enabled/default
state: absent
- name: Enable {{ domain_name }} site
ansible.builtin.file:
src: "/etc/nginx/sites-available/{{ domain_name }}"
dest: "/etc/nginx/sites-enabled/{{ domain_name }}"
state: link
force: true
- name: Test nginx configuration
ansible.builtin.command: nginx -t
register: nginx_test
changed_when: false
- name: Reload nginx
ansible.builtin.service:
name: nginx
state: reloaded
- name: Ensure nginx reloads on renewal
ansible.builtin.copy:
dest: /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
content: |
#!/bin/sh
/bin/systemctl reload nginx
owner: root
group: root
mode: "0755"
- name: Add cron to renew twice daily
ansible.builtin.cron:
name: "Certbot renew"
user: root
minute: "0"
hour: "*/12"
job: "/usr/bin/certbot renew -q"
# --- Google Cloud CLI install and configuration ---
- name: Install Google Cloud CLI (role)
ansible.builtin.include_role:
name: google.cloud.gcloud
- name: Ensure gcloud key directory exists
ansible.builtin.file:
path: /root/.gcloud
state: directory
owner: root
group: root
mode: "0700"
when: (gcloud_sa_email | length) > 0 and (gcloud_sa_key_json | length) > 0
- name: Write GCP service account key (decoded from base64)
ansible.builtin.copy:
dest: /root/.gcloud/sa.json
content: "{{ gcloud_sa_key_json | b64decode }}"
owner: root
group: root
mode: "0600"
when: (gcloud_sa_email | length) > 0 and (gcloud_sa_key_json | length) > 0
- name: Check existing gcloud authenticated accounts
ansible.builtin.command: gcloud auth list --format=value(account)
register: gcloud_auth_list
changed_when: false
failed_when: false
environment:
CLOUDSDK_CORE_DISABLE_PROMPTS: "1"
when: (gcloud_sa_email | length) > 0 and (gcloud_sa_key_json | length) > 0
- name: Activate service account for gcloud
ansible.builtin.command: >-
gcloud auth activate-service-account {{ gcloud_sa_email }}
--key-file=/root/.gcloud/sa.json --quiet
environment:
CLOUDSDK_CORE_DISABLE_PROMPTS: "1"
when: (gcloud_sa_email | length) > 0 and (gcloud_sa_key_json | length) > 0 and (gcloud_sa_email not in (gcloud_auth_list.stdout_lines | default([])))
- name: Configure Docker auth for Artifact Registry
ansible.builtin.command: >-
gcloud auth configure-docker {{ gcloud_registry_host }} --quiet
environment:
CLOUDSDK_CORE_DISABLE_PROMPTS: "1"
when: (gcloud_sa_email | length) > 0 and (gcloud_sa_key_json | length) > 0
# --- Docker Engine + Compose install ---
- name: Install Docker prereqs
ansible.builtin.apt:
name:
- ca-certificates
- curl
- gnupg
- lsb-release
state: present
update_cache: true
- name: Ensure keyrings directory exists
ansible.builtin.file:
path: /etc/apt/keyrings
state: directory
mode: "0755"
- name: Add Docker GPG key
ansible.builtin.get_url:
url: https://download.docker.com/linux/ubuntu/gpg
dest: /etc/apt/keyrings/docker.asc
mode: "0644"
force: true
- name: Determine deb arch for Docker repo
ansible.builtin.set_fact:
docker_deb_arch: >-
{{ 'arm64' if ansible_architecture in ['aarch64', 'arm64'] else 'amd64' }}
- name: Add Docker apt repository
ansible.builtin.apt_repository:
repo: >-
deb [arch={{ docker_deb_arch }} signed-by=/etc/apt/keyrings/docker.asc]
https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable
filename: docker
state: present
update_cache: true
- name: Install Docker Engine and Compose plugin
ansible.builtin.apt:
name:
- docker-ce
- docker-ce-cli
- containerd.io
- docker-buildx-plugin
- docker-compose-plugin
state: present
- name: Enable and start Docker
ansible.builtin.service:
name: docker
state: started
enabled: true
- name: Add users to docker group
ansible.builtin.user:
name: "{{ item }}"
groups: docker
append: true
loop: "{{ docker_users | default([]) }}"
ignore_errors: true
# --- Deploy docker-compose app and start services ---
- name: Ensure app root exists
ansible.builtin.file:
path: "{{ app_root }}"
state: directory
mode: "0755"
- name: Copy docker-compose.yaml to host
ansible.builtin.copy:
src: "{{ playbook_dir }}/../../docker-compose.yaml"
dest: "{{ app_root }}/docker-compose.yaml"
mode: "0644"
- name: Render .env file
ansible.builtin.template:
src: ../templates/env.j2
dest: "{{ app_root }}/.env"
mode: "0600"
- name: Pull images
ansible.builtin.command: docker compose -f {{ app_root }}/docker-compose.yaml pull
args:
chdir: "{{ app_root }}"
- name: Start services
ansible.builtin.command: docker compose -f {{ app_root }}/docker-compose.yaml up -d
args:
chdir: "{{ app_root }}"

5
ansible/requirements.yml Normal file
View File

@@ -0,0 +1,5 @@
---
collections:
- name: community.general
- name: google.cloud
- name: community.docker

View File

@@ -0,0 +1 @@
dns_cloudflare_api_token = {{ cloudflare_api_token }}

8
ansible/templates/env.j2 Normal file
View File

@@ -0,0 +1,8 @@
GCS_KEY_ENCODED={{ gcs_key_encoded }}
HOOK_TOKEN={{ hook_token }}
HOOK_URI={{ hook_uri }}
REDIS_PASSWORD={{ redis_password }}
REDISCLOUD_URL={{ rediscloud_url }}
REPLREG_HOST={{ replreg_host }}
REPLREG_SECRET={{ replreg_secret }}
REGISTRY_URL={{ registry_url }}

View File

@@ -0,0 +1,47 @@
server {
listen 80;
server_name {{ domain_name }};
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name {{ domain_name }};
ssl_certificate /etc/letsencrypt/live/{{ domain_name }}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/{{ domain_name }}/privkey.pem;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
root /var/www/{{ domain_name }}/html;
index index.htm index.html;
# Static site
location / {
try_files $uri $uri/ =404;
}
# Block listing catalog
location ~ v2/_catalog$ {
return 401;
}
# Docker Registry proxy
location /v2 {
client_max_body_size 10000m;
client_body_timeout 3600s;
proxy_connect_timeout 300s;
proxy_send_timeout 3600s;
proxy_read_timeout 3600s;
send_timeout 3600s;
proxy_pass http://127.0.0.1:5000;
proxy_set_header X-Forwarded-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;
proxy_set_header Host $host;
proxy_request_buffering off;
}
}

View File

@@ -69,26 +69,6 @@ services:
- ./redis/data:/data
command: redis-server --requirepass ${REDIS_PASSWORD}
nginx:
image: us-east4-docker.pkg.dev/rgstry/ttl-sh-nginx/nginx:latest
build:
context: nginx
dockerfile: Dockerfile
platforms:
- "linux/amd64"
container_name: ttlsh-nginx
restart: always
pull_policy: always
environment:
- PORT=80
ports:
- "80:80"
- "443:443"
depends_on:
- registry
networks:
- ttlsh-network
networks:
ttlsh-network:
driver: bridge

View File

@@ -1,27 +0,0 @@
#!/usr/bin/env bash
REMOTE_HOST=178.156.198.215
REMOTE_USER=root
INSTALL_DIR=/opt/ttlsh
function setup_docker_compose() {
ssh $REMOTE_USER@$REMOTE_HOST "mkdir -p $INSTALL_DIR"
scp docker-compose.yaml $REMOTE_USER@$REMOTE_HOST:$INSTALL_DIR/docker-compose.yaml
}
function setup_provisioner_script() {
scp provision.sh $REMOTE_USER@$REMOTE_HOST:$INSTALL_DIR/provision.sh
ssh $REMOTE_USER@$REMOTE_HOST "chmod a+x $INSTALL_DIR/provision.sh"
}
function run_provisioner_script() {
ssh $REMOTE_USER@$REMOTE_HOST "bash $INSTALL_DIR/provision.sh $1"
}
function install() {
setup_docker_compose
setup_provisioner_script
run_provisioner_script $DOPPLER_TOKEN
}
install

View File

@@ -1,12 +0,0 @@
FROM nginx:latest
COPY nginx.conf /etc/nginx/nginx.conf
COPY src/favicon.ico /usr/share/nginx/html/
COPY src/index.htm /usr/share/nginx/html/
COPY src/ttlsh_files /usr/share/nginx/html/ttlsh_files/
ADD entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -1,7 +0,0 @@
#!/bin/sh
set -e
sed -i "s/__PORT__/$PORT/g" /etc/nginx/nginx.conf
exec "$@"

View File

@@ -1,39 +0,0 @@
events {}
http {
include mime.types;
default_type application/octet-stream;
upstream registry {
server ttlsh-registry:5000;
}
server {
listen __PORT__;
server_name _;
location / {
root /usr/share/nginx/html;
index index.htm;
}
location ~ v2/_catalog$ {
return 401;
}
location /v2 {
client_max_body_size 10000m;
client_body_timeout 3600s;
proxy_connect_timeout 300s;
proxy_send_timeout 3600s;
proxy_read_timeout 3600s;
send_timeout 3600s;
proxy_pass http://registry;
proxy_set_header X-Forwarded-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;
proxy_set_header Host $host;
proxy_request_buffering off;
}
}
}

View File

@@ -1,94 +0,0 @@
#!/usr/bin/env bash
DOPPLER_TOKEN=$1
function setup_doppler() {
# install for ubuntu: https://docs.doppler.com/docs/cli
sudo apt-get update && sudo apt-get install -y apt-transport-https ca-certificates curl gnupg
curl -sLf --retry 3 --tlsv1.2 --proto "=https" 'https://packages.doppler.com/public/cli/gpg.DE2A7741A397C129.key' | sudo gpg --dearmor -o /usr/share/keyrings/doppler-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/doppler-archive-keyring.gpg] https://packages.doppler.com/public/cli/deb/debian any-version main" | sudo tee /etc/apt/sources.list.d/doppler-cli.list
sudo apt-get update && sudo apt-get install doppler
# authenticate with service token
echo $DOPPLER_TOKEN | doppler configure set token --scope /
}
setup_docker() {
# Adapted from:
# * https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository
# Add Docker's official GPG key:
sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
# Add the repository to Apt sources:
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
# install necessary packages
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin -y
}
setup_gcloud() {
# Adapted from:
# * https://cloud.google.com/sdk/docs/downloads-interactive
# * Artifact Registry setup commands
INSTALL_PATH="$(doppler secrets get GOOGLE_APPLICATION_CREDENTIALS --plain)"
INSTALL_EMAIL="$(doppler secrets get GOOGLE_APPLICATION_CREDENTIALS_EMAIL --plain)"
REGISTRY_URI="$(doppler secrets get GOOGLE_CLOUD_ARTIFACT_REGISTRY_URI --plain)"
mkdir -p "$(dirname "$INSTALL_PATH")"
doppler secrets get GOOGLE_APPLICATION_CREDENTIALS_JSON --plain >"$INSTALL_PATH"
chmod 600 "$INSTALL_PATH"
echo "export GOOGLE_APPLICATION_CREDENTIALS=\"$INSTALL_PATH\"" >> "$HOME/.bashrc"
# download the installer
curl https://sdk.cloud.google.com | bash
# Explicitly add gcloud to PATH for this session
export PATH="$HOME/google-cloud-sdk/bin:$PATH"
gcloud auth activate-service-account "$INSTALL_EMAIL" --key-file="$INSTALL_PATH" --quiet
gcloud auth configure-docker "$REGISTRY_URI" --quiet
}
setup_ttlsh_env() {
for VAR in \
GCS_KEY_ENCODED \
HOOK_TOKEN \
HOOK_URI \
REDIS_PASSWORD \
REDISCLOUD_URL \
REPLREG_HOST \
REPLREG_SECRET \
REGISTRY_URL
do
echo "$VAR=$(doppler secrets get "$VAR" --plain)" >> /opt/ttlsh/.env
done
chmod 600 "$ENV_FILE"
}
function run_ttlsh()
{
docker compose -f /opt/ttlsh/docker-compose.yaml pull
docker compose -f /opt/ttlsh/docker-compose.yaml up -d
}
function provision() {
setup_doppler
setup_docker
setup_gcloud
setup_ttlsh_env
run_ttlsh
}
provision

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB