feat: Working minimal, reproducible infrastructure.
commit
b470f36da0
|
@ -0,0 +1,35 @@
|
|||
|
||||
# You may want to customise this file depending on your Operating System
|
||||
# and the editor that you use.
|
||||
#
|
||||
# We recommend that you use a Global Gitignore for files that are not related
|
||||
# to the project. (https://help.github.com/articles/ignoring-files/#create-a-global-gitignore)
|
||||
|
||||
# OS
|
||||
#
|
||||
# Ref: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore
|
||||
# Ref: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore
|
||||
# Ref: https://github.com/github/gitignore/blob/master/Global/Linux.gitignore
|
||||
.DS_STORE
|
||||
Thumbs.db
|
||||
|
||||
# Editors
|
||||
#
|
||||
# Ref: https://github.com/github/gitignore/blob/master/Global
|
||||
# Ref: https://github.com/github/gitignore/blob/master/Global/JetBrains.gitignore
|
||||
# Ref: https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore
|
||||
.idea
|
||||
.chrome
|
||||
/*.log
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
|
||||
# Python
|
||||
**/__pycache__
|
||||
.venv
|
||||
|
||||
# Local Developer Notes
|
||||
dev
|
|
@ -0,0 +1,22 @@
|
|||
# EditorConfig is awesome: https://EditorConfig.org
|
||||
root = true
|
||||
|
||||
# Unix-style newlines with a newline ending every file
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
|
||||
# Python
|
||||
[*.py]
|
||||
indent_style = tab
|
||||
indent_size = 2
|
||||
|
||||
# Python
|
||||
[*.toml]
|
||||
indent_style = tab
|
||||
indent_size = 2
|
||||
|
||||
# YML
|
||||
[*.(yml|yaml)]
|
||||
indent_style = space
|
||||
indent_size = 2
|
|
@ -0,0 +1,40 @@
|
|||
|
||||
# You may want to customise this file depending on your Operating System
|
||||
# and the editor that you use.
|
||||
#
|
||||
# We recommend that you use a Global Gitignore for files that are not related
|
||||
# to the project. (https://help.github.com/articles/ignoring-files/#create-a-global-gitignore)
|
||||
|
||||
# OS
|
||||
#
|
||||
# Ref: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore
|
||||
# Ref: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore
|
||||
# Ref: https://github.com/github/gitignore/blob/master/Global/Linux.gitignore
|
||||
.DS_STORE
|
||||
Thumbs.db
|
||||
|
||||
# Editors
|
||||
#
|
||||
# Ref: https://github.com/github/gitignore/blob/master/Global
|
||||
# Ref: https://github.com/github/gitignore/blob/master/Global/JetBrains.gitignore
|
||||
# Ref: https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore
|
||||
.idea
|
||||
.chrome
|
||||
/*.log
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
**/neovide_backtraces.log
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
.venv
|
||||
.cache-trivy/
|
||||
.hypothesis/
|
||||
|
||||
# Local Developer Notes
|
||||
dev
|
|
@ -0,0 +1,7 @@
|
|||
repos:
|
||||
- repo: https://github.com/compilerla/conventional-pre-commit
|
||||
rev: v2.3.0
|
||||
hooks:
|
||||
- id: conventional-pre-commit
|
||||
stages: [commit-msg]
|
||||
args: [feat, fix, ci, chore] # list of Conventional Commits types to allow
|
|
@ -0,0 +1,67 @@
|
|||
# Prerequisites
|
||||
|
||||
## Wireguard Key Generation
|
||||
*TODO: Automate?*
|
||||
|
||||
Generate wg keys for all hosts:
|
||||
```bash
|
||||
wg genkey
|
||||
pass insert path/to/private
|
||||
pass /path/to/private | wg pubkey
|
||||
pass insert /path/to/public
|
||||
```
|
||||
|
||||
Save each in `password-store` under `<host>_<private|public>_key`.
|
||||
|
||||
Then, generate a "Pre-Shared Key" for each Peer-Peer:
|
||||
```
|
||||
wg genpsk > psk_peer_peer
|
||||
```
|
||||
|
||||
|
||||
|
||||
# Persistence
|
||||
This deployment has the following requirements in terms of persistence:
|
||||
|
||||
## auth
|
||||
`authentik-postgres`:
|
||||
1. **Low-Latency FS**: Storage for `postgres` database.
|
||||
2. **FS**: Storage for `postgres` backups.
|
||||
|
||||
`authentik-redis`:
|
||||
1. **FS** (*non-critical*): Storage for RDB + AOF Redis persistence.
|
||||
|
||||
## chat
|
||||
`zulip-postgres`
|
||||
1. **Low-Latency**: Storage for `postgres` database.
|
||||
2. **FS**: Storage for `postgres` backups.
|
||||
|
||||
`zulip-redis`:
|
||||
1. **FS** (*non-critical*): Storage for RDB + AOF Redis persistence.
|
||||
|
||||
`zulip`:
|
||||
1. **FS/S3**: Storage for file uploads.
|
||||
|
||||
## git
|
||||
`gitea`:
|
||||
1. **FS/S3**: Attachments, lfs, avatars, repo-avatars, repo-archive, packages, actions_log, actions_artifact
|
||||
2. **FS**: Repository Storage.
|
||||
3. **Low-Latency FS**: Postgres Storage.
|
||||
4. **Low-Latency FS**: Indexer (mellisearch) storage.
|
||||
5. **FS**: Storage for `SQLite` backups.
|
||||
|
||||
`gitea-redis`:
|
||||
1. **FS** (*non-critical*): Storage for RDB + AOF Redis persistence.
|
||||
|
||||
## mesh
|
||||
`traefik`:
|
||||
1. **FS** (*sensitive*): Storage for SSL Certificates.
|
||||
|
||||
## updater
|
||||
`diun`:
|
||||
1. **Low-Latency FS** (*non-critical*): Cache for Previous Image Updates.
|
||||
|
||||
## uptime
|
||||
`uptime-kuma`:
|
||||
1. **Low-Latency FS**: Storage for SQLite DB.
|
||||
- **NOTE: We might be able to remove this by configuring it on startup.**
|
|
@ -0,0 +1,60 @@
|
|||
# Complete Infrastructure for DTU Python Support
|
||||
**Very heavily WIP**
|
||||
|
||||
This project describes and implements the complete infrastructure for DTUs Python Support group.
|
||||
|
||||
The repository provides the following user-facing services:
|
||||
- timesigned.com: Modern, multilingual guide to using Python at DTU.
|
||||
- SSG with [mdbook](https://rust-lang.github.io/mdBook/) w/plugins.
|
||||
|
||||
- chat.timesigned.com: Modern asynchronous communication and support channel for everybody using Python at DTU.
|
||||
- Instance of [Zulip](https://zulip.com/).
|
||||
|
||||
- git.timesigned.com: Lightweight collaborative development for teams
|
||||
- Instance of [Forgejo](https://forgejo.org/), itself a soft-fork of [Gitea](https://about.gitea.com/)
|
||||
|
||||
- auth.timesigned.com: Identity Provider allowing seamless, secure access to key services with their DTU Account.
|
||||
- Instance of [Authentik](https://goauthentik.io/).
|
||||
|
||||
- uptime.timesigned.com: Black-box monitoring with notifications
|
||||
- Instance of [Authentik](https://goauthentik.io/).
|
||||
|
||||
|
||||
|
||||
# References
|
||||
|
||||
## Wireguard / systemd-networkd
|
||||
- `systemd-networkd` Network: <https://www.freedesktop.org/software/systemd/man/systemd.network.html>
|
||||
- `systemd-networkd` NetDev: <https://man.archlinux.org/man/systemd.netdev.5>
|
||||
- Setup Inspiration: <https://elou.world/en/tutorial/wireguard>
|
||||
- Wireguard w/`systemd-networkd`: <https://wiki.archlinux.org/title/WireGuard#systemd-networkd>
|
||||
- Network Test w/`iperf`: <https://www.redhat.com/sysadmin/network-testing-iperf3>
|
||||
|
||||
## Ansible
|
||||
- DigitalOcean `droplet`: <https://docs.ansible.com/ansible/latest/collections/community/digitalocean/digital_ocean_droplet_module.html>
|
||||
- CloudFlare `dns`: <https://docs.ansible.com/ansible/latest/collections/community/general/cloudflare_dns_module.html>
|
||||
- `template`: <https://docs.ansible.com/ansible/latest/collections/ansible/builtin/template_module.html>
|
||||
- `password-store`: <https://docs.ansible.com/ansible/latest/collections/community/general/passwordstore_lookup.html>
|
||||
- `set-fact`: <https://docs.ansible.com/ansible/latest/collections/ansible/builtin/set_fact_module.html>
|
||||
- `file`: <https://docs.ansible.com/ansible/latest/collections/ansible/builtin/file_module.html>
|
||||
|
||||
### Docker Ansible
|
||||
- Index: <https://docs.ansible.com/ansible/latest/collections/community/docker/index.html>
|
||||
- Docker `swarm` Module: <https://docs.ansible.com/ansible/latest/collections/community/docker/docker_swarm_module.html>
|
||||
- Docker `network` Module: <https://docs.ansible.com/ansible/latest/collections/community/docker/docker_network_module.html>
|
||||
- Docker `prune` Module: <https://docs.ansible.com/ansible/latest/collections/community/docker/docker_prune_module.html>
|
||||
- Docker `volume` Module: <https://docs.ansible.com/ansible/latest/collections/community/docker/docker_volume_module.html>
|
||||
|
||||
## rclone
|
||||
- Docker Plugin Docs: <https://rclone.org/docker/>
|
||||
- `rclone` mount: <https://rclone.org/commands/rclone_mount/>
|
||||
- Docker Serve Docs: <https://rclone.org/commands/rclone_serve_docker/#options>
|
||||
|
||||
- S3 Backend: <https://rclone.org/s3/>
|
||||
- Crypt Meta-Backend: <https://rclone.org/crypt/>
|
||||
|
||||
|
||||
## Swarm Deployment
|
||||
- The Funky Penguin: <https://geek-cookbook.funkypenguin.co.nz/docker-swarm>
|
||||
- Traefik Certificate Auto-Renewal: <https://doc.traefik.io/traefik/https/acme/#automatic-renewals>
|
||||
- Traefik Service: <https://doc.traefik.io/traefik/routing/services/#configuring-http-services>
|
|
@ -0,0 +1,149 @@
|
|||
# Ansible / Dev TODO
|
||||
Cluster/Ansible Setup
|
||||
- [x] Setup Playbook
|
||||
- [x] Root as local var: `work/dtu/python-support/*`
|
||||
- [x] Get 2 DO Droplets
|
||||
- [x] Provision DNS
|
||||
- [ ] Key Fingerprint as local var
|
||||
- [x] Setup Wireguard wg0 between DO Droplets
|
||||
- [ ] Setup unattended-upgrades
|
||||
|
||||
Swarm
|
||||
- [x] Install Docker
|
||||
- [x] Check Swarm ports on wg0: https://docs.docker.com/engine/swarm/swarm-tutorial/
|
||||
- [x] Init Swarm manager & worker
|
||||
- [x] Install rclone volume plugin: https://rclone.org/docker/
|
||||
- [ ] Label big one as 'storage'
|
||||
|
||||
Stack: cleanup
|
||||
- [x] Security Audit
|
||||
- [x] **Deploy Stack**
|
||||
|
||||
Stack: mesh
|
||||
- [x] Install Configs
|
||||
- [x] **Deploy Stack**
|
||||
|
||||
- [x] rclone `acme.json` to R2 w/crypt
|
||||
- [ ] Security Audit
|
||||
|
||||
Stack: site-support
|
||||
- [x] Generate Configs
|
||||
- [x] Install Configs
|
||||
- [x] **Deploy Stack**
|
||||
|
||||
- [ ] Security Audit
|
||||
|
||||
Stack: updater
|
||||
- [ ] config: main
|
||||
- [ ] config: cleanup
|
||||
- [ ] config: mesh
|
||||
- [ ] config: site-support
|
||||
- [ ] Install Configs
|
||||
- [ ] **Deploy Stack**
|
||||
|
||||
- [ ] Security Audit
|
||||
|
||||
Stack: auth
|
||||
- [ ] Write Stack
|
||||
- [ ] storage: authentik-postgres
|
||||
- [ ] storage: authentik-redis
|
||||
- [ ] *Test Deploy*
|
||||
|
||||
- [ ] configs: Blueprints (export from prototyping)
|
||||
- [ ] Install Configs
|
||||
- [ ] role: API Setup of Things
|
||||
- [ ] **Deploy Stack**
|
||||
|
||||
- [ ] updater: Integrate update-check
|
||||
- [ ] Security Audit
|
||||
|
||||
Stack: s3
|
||||
- [ ] Write Stack
|
||||
- https://geek-cookbook.funkypenguin.co.nz/recipes/minio/
|
||||
- Restrict to 'storage' label.
|
||||
|
||||
- [ ] ...?
|
||||
- [ ] Install Configs
|
||||
- [ ] Install Secrets
|
||||
- [ ] storage: minio
|
||||
- [ ] *Test Deploy*
|
||||
|
||||
- [ ] role: API Setup of Things
|
||||
- [ ] **Deploy Stack**
|
||||
|
||||
- [ ] auth: Integrate OIDC
|
||||
- https://min.io/docs/minio/container/operations/external-iam.html
|
||||
- https://goauthentik.io/integrations/services/minio/
|
||||
- [ ] updater: integrate
|
||||
- [ ] Security Audit
|
||||
|
||||
Stack: chat
|
||||
- [ ] Write Stack
|
||||
- https://geek-cookbook.funkypenguin.co.nz/recipes/minio/
|
||||
- Restrict to 'storage' label.
|
||||
|
||||
- [ ] ...?
|
||||
- [ ] Install Configs
|
||||
- [ ] Install Secrets
|
||||
- [ ] storage: zulip-postgres
|
||||
- [ ] storage: zulip-rabbitmq
|
||||
- [ ] storage: zulip-redis
|
||||
- [ ] s3: zulip
|
||||
- [ ] *Test Deploy*
|
||||
|
||||
- [ ] auth: Integrate OIDC
|
||||
- https://zulip.readthedocs.io/en/latest/production/authentication-methods.html#openid-connect
|
||||
- Backup SAML: https://goauthentik.io/integrations/services/zulip/
|
||||
- [ ] role: API Setup of Things
|
||||
- [ ] **Deploy Stack**
|
||||
|
||||
- [ ] updater: Integrate
|
||||
- [ ] Security Audit
|
||||
|
||||
Stack: git
|
||||
- [ ] Install Configs
|
||||
- [ ] Install Secrets
|
||||
- [ ] *Test Deploy*
|
||||
|
||||
- [ ] storage: gitea-redis
|
||||
- [ ] storage: gitea-postgres
|
||||
- [ ] storage: gitea-mellisearch
|
||||
- https://www.meilisearch.com/docs/learn/cookbooks/docker
|
||||
- [ ] s3: gitea
|
||||
- [ ] s3 via rclone: gitea (repositories)
|
||||
- [ ] role: API Setup of Things
|
||||
- [ ] **Deploy Stack**
|
||||
|
||||
- [ ] Configure gitea-actions w/auto-setup
|
||||
- [ ] manual: Migrate docker-mdbook, site-support.
|
||||
|
||||
|
||||
Bonus:
|
||||
- Play with `uptime`.
|
||||
- Backups!
|
||||
|
||||
|
||||
|
||||
# Playbook Creation Notes
|
||||
- [x] mesh should use a non-`local` driver.
|
||||
- [ ] Implement rolling updates to services within stacks, whose configs have changed.
|
||||
- Note `rolling_updates` in the `docker_config` ansible module.
|
||||
- With a little information-gathering, I'm certain we can prevent actually stopping stacks on deploy and instead only do the secret rotation as described in the Docker documentation: https://docs.docker.com/engine/swarm/secrets/#example-rotate-a-secret
|
||||
- NOTE that the rclone volume stuff is always gonna need manual stop/start. Is jank. Such is the life.
|
||||
|
||||
- [ ] Automatic R2 Bucket Creation
|
||||
- [ ] Only do the delays when we actually need to stop stacks / unmount volumes
|
||||
|
||||
- [ ] Encrypted use of R2 bucket.
|
||||
- https://rclone.org/crypt/
|
||||
|
||||
- [ ] Templated security.txt in site-support
|
||||
- [ ] Templated limits to not kill the demo hosts in ex. site-support :)
|
||||
|
||||
- [ ] Please, please, a nice README.md in site-support?
|
||||
|
||||
- [ ] Move DNS stuff out to the stacks. Trust me!
|
||||
- [ ] Invest in some delegation to roles. These playbooks be gettin messy.
|
||||
|
||||
- [ ] Figure out a way to deal with concurrent `acme.json` in Traefik. For now I've set it to one replica and `vfs_cache_mode=full` (I think `none` may be wonky with this particular need of Traefik?)
|
||||
- Needs more testing!
|
|
@ -0,0 +1,65 @@
|
|||
####################
|
||||
# - Hosts - by Purpose
|
||||
####################
|
||||
service:
|
||||
hosts:
|
||||
raspberry.node.timesigned.com:
|
||||
vars:
|
||||
ansible_user: root
|
||||
|
||||
storage:
|
||||
hosts:
|
||||
blueberry.node.timesigned.com:
|
||||
vars:
|
||||
ansible_user: root
|
||||
|
||||
####################
|
||||
# - Hosts - by Swarm Role
|
||||
####################
|
||||
leader:
|
||||
## ONLY ==1 Host can be Leader
|
||||
hosts:
|
||||
raspberry.node.timesigned.com:
|
||||
vars:
|
||||
ansible_user: root
|
||||
|
||||
manager:
|
||||
hosts:
|
||||
raspberry.node.timesigned.com:
|
||||
vars:
|
||||
ansible_user: root
|
||||
|
||||
worker:
|
||||
hosts:
|
||||
blueberry.node.timesigned.com:
|
||||
vars:
|
||||
ansible_user: root
|
||||
|
||||
swarm:
|
||||
hosts:
|
||||
raspberry.node.timesigned.com:
|
||||
blueberry.node.timesigned.com:
|
||||
vars:
|
||||
ansible_user: root
|
||||
|
||||
####################
|
||||
# - Hosts - by L3 Network
|
||||
####################
|
||||
wg0:
|
||||
hosts:
|
||||
raspberry.node.timesigned.com:
|
||||
wg0_ip: "10.9.8.1"
|
||||
|
||||
wg_private_key: "{{ lookup('community.general.passwordstore', 'work/dtu/python-support/wg/raspberry_private_key') }}"
|
||||
wg_public_key: "{{ lookup('community.general.passwordstore', 'work/dtu/python-support/wg/raspberry_public_key') }}"
|
||||
|
||||
wg_psk_blueberry.node.timesigned.com: "{{ lookup('community.general.passwordstore', 'work/dtu/python-support/wg/psk_raspberry-blueberry') }}"
|
||||
blueberry.node.timesigned.com:
|
||||
wg0_ip: "10.9.8.2"
|
||||
|
||||
wg_private_key: "{{ lookup('community.general.passwordstore', 'work/dtu/python-support/wg/blueberry_private_key') }}"
|
||||
wg_public_key: "{{ lookup('community.general.passwordstore', 'work/dtu/python-support/wg/blueberry_public_key') }}"
|
||||
|
||||
wg_psk_raspberry.node.timesigned.com: "{{ lookup('community.general.passwordstore', 'work/dtu/python-support/wg/psk_raspberry-blueberry') }}"
|
||||
vars:
|
||||
ansible_user: root
|
|
@ -0,0 +1,136 @@
|
|||
- hosts: localhost
|
||||
vars:
|
||||
dns_root: "timesigned.com"
|
||||
node_primary: "raspberry.node.timesigned.com"
|
||||
|
||||
digitalocean_droplet_token: "{{ lookup('community.general.passwordstore', 'work/dtu/python-support/digitalocean-droplet-token') }}"
|
||||
|
||||
cloudflare_email: "{{ lookup('community.general.passwordstore', 'work/dtu/python-support/cloudflare-email') }}"
|
||||
cloudflare_dns_token: "{{ lookup('community.general.passwordstore', 'work/dtu/python-support/cloudflare-dns-token') }}"
|
||||
|
||||
droplet_service_image: "debian-12-x64"
|
||||
## curl -X GET --silent "https://api.digitalocean.com/v2/images?per_page=999" -H "Authorization: Bearer $(pass work/dtu/python-support/digitalocean-droplet-token)" | jq | less
|
||||
droplet_service_size: "s-1vcpu-1gb"
|
||||
droplet_service_region: "fra1"
|
||||
## curl -X GET --silent "https://api.digitalocean.com/v2/sizes?per_page=999" -H "Authorization: Bearer $(pass work/dtu/python-support/digitalocean-droplet-token)" | jq | less
|
||||
|
||||
droplet_storage_image: "debian-12-x64"
|
||||
droplet_storage_size: "s-1vcpu-1gb"
|
||||
droplet_storage_region: "fra1"
|
||||
|
||||
tasks:
|
||||
####################
|
||||
# - Prepare SSH Information
|
||||
####################
|
||||
- name: "Get SSH Public Key"
|
||||
shell: "ssh-add -L"
|
||||
register: "ssh_key_pub_cmdout"
|
||||
|
||||
- name: "Add SSH Public Key to DigitalOcean account"
|
||||
digital_ocean_sshkey:
|
||||
name: "key"
|
||||
oauth_token: "{{ digitalocean_droplet_token }}"
|
||||
ssh_pub_key: "{{ ssh_key_pub_cmdout.stdout }}"
|
||||
state: "present"
|
||||
register: "sshkey_result"
|
||||
|
||||
####################
|
||||
# - Create Digitalocean Nodes
|
||||
####################
|
||||
- name: "Create Storage Droplet"
|
||||
digital_ocean_droplet:
|
||||
name: "{{ item }}"
|
||||
oauth_token: "{{ digitalocean_droplet_token }}"
|
||||
ssh_keys: ["{{ sshkey_result.data.ssh_key.id }}"]
|
||||
|
||||
image: "{{ droplet_storage_image }}"
|
||||
size: "{{ droplet_storage_size }}"
|
||||
region: "{{ droplet_storage_region }}"
|
||||
|
||||
wait_timeout: 600
|
||||
unique_name: "yes"
|
||||
|
||||
state: present
|
||||
with_inventory_hostnames:
|
||||
- storage
|
||||
register: droplet_storage_result
|
||||
|
||||
- name: "Create Service Droplet"
|
||||
digital_ocean_droplet:
|
||||
name: "{{ item }}"
|
||||
oauth_token: "{{ digitalocean_droplet_token }}"
|
||||
ssh_keys: ["{{ sshkey_result.data.ssh_key.id }}"]
|
||||
|
||||
image: "{{ droplet_service_image }}"
|
||||
size: "{{ droplet_service_size }}"
|
||||
region: "{{ droplet_service_region }}"
|
||||
|
||||
wait_timeout: 600
|
||||
unique_name: "yes"
|
||||
|
||||
state: present
|
||||
with_inventory_hostnames:
|
||||
- service
|
||||
register: droplet_service_result
|
||||
|
||||
####################
|
||||
# - Set DNS A Records => Hosts
|
||||
####################
|
||||
- name: "Set Storage DNS A => *.node.{{ dns_root }}"
|
||||
cloudflare_dns:
|
||||
api_token: "{{ cloudflare_dns_token }}"
|
||||
|
||||
zone: "{{ dns_root }}"
|
||||
type: "A"
|
||||
|
||||
record: "{{ item.data.droplet.name }}"
|
||||
value: "{{ item.data.ip_address }}"
|
||||
with_items: "{{ droplet_storage_result.results }}"
|
||||
|
||||
- name: "Set Service DNS A => *.node.{{ dns_root }}"
|
||||
cloudflare_dns:
|
||||
api_token: "{{ cloudflare_dns_token }}"
|
||||
|
||||
zone: "{{ dns_root }}"
|
||||
type: "A"
|
||||
|
||||
record: "{{ item.data.droplet.name }}"
|
||||
value: "{{ item.data.ip_address }}"
|
||||
with_items: "{{ droplet_service_result.results }}"
|
||||
|
||||
####################
|
||||
# - Set DNS CNAME Record => @
|
||||
####################
|
||||
- name: "Set DNS CNAME => Primary Node"
|
||||
cloudflare_dns:
|
||||
api_token: "{{ cloudflare_dns_token }}"
|
||||
|
||||
zone: "{{ dns_root }}"
|
||||
type: "CNAME"
|
||||
|
||||
record: "@"
|
||||
value: "{{ node_primary }}"
|
||||
## Cloudflare allows CNAME on @ via CNAME-flattening
|
||||
|
||||
####################
|
||||
# - Set DNS CNAME Records => Stacks
|
||||
####################
|
||||
- name: "Set DNS CNAME => Stack: auth"
|
||||
cloudflare_dns:
|
||||
api_token: "{{ cloudflare_dns_token }}"
|
||||
|
||||
zone: "{{ dns_root }}"
|
||||
type: "CNAME"
|
||||
|
||||
record: "auth"
|
||||
value: "@"
|
||||
|
||||
- name: "Set DNS CNAME => Stack: site-support"
|
||||
cloudflare_dns:
|
||||
api_token: "{{ cloudflare_dns_token }}"
|
||||
|
||||
zone: "{{ dns_root }}"
|
||||
type: "CNAME"
|
||||
|
||||
record: "pysupport"
|
||||
value: "@"
|
|
@ -0,0 +1,132 @@
|
|||
- hosts: swarm
|
||||
become: "true"
|
||||
tasks:
|
||||
####################
|
||||
# - Tuning - Traefik
|
||||
# -- Traefik serving QUIC can be bottlenecked by a too-low UDP buffer.
|
||||
# -- This increases both send & receive from ~200KB to 2.5MB.
|
||||
####################
|
||||
- name: "Set net.core.rmem_max = 2500000"
|
||||
sysctl:
|
||||
state: "present"
|
||||
name: "net.core.rmem_max"
|
||||
value: "2500000"
|
||||
reload: "yes"
|
||||
|
||||
- name: "Set net.core.wmem_max = 2500000"
|
||||
sysctl:
|
||||
state: "present"
|
||||
name: "net.core.rmem_max"
|
||||
value: "2500000"
|
||||
reload: "yes"
|
||||
|
||||
####################
|
||||
# - Docker - Install
|
||||
####################
|
||||
- name: "Download Docker Apt Key"
|
||||
ansible.builtin.get_url:
|
||||
url: "https://download.docker.com/linux/debian/gpg"
|
||||
dest: "/etc/apt/trusted.gpg.d/docker.asc"
|
||||
checksum: "sha256:1500c1f56fa9e26b9b8f42452a553675796ade0807cdce11975eb98170b3a570"
|
||||
owner: "root"
|
||||
group: "root"
|
||||
mode: "644"
|
||||
|
||||
- name: "Add Docker Apt Repository"
|
||||
apt_repository:
|
||||
state: "present"
|
||||
repo: "deb https://download.docker.com/linux/debian bullseye stable"
|
||||
filename: "docker"
|
||||
|
||||
- name: "Install Docker CE"
|
||||
apt:
|
||||
state: "present"
|
||||
name: "docker-ce"
|
||||
|
||||
- name: "Install python3-docker"
|
||||
apt:
|
||||
state: "present"
|
||||
name: "python3-docker"
|
||||
|
||||
####################
|
||||
# - Docker Plugin - rclone
|
||||
####################
|
||||
- name: "Install fuse"
|
||||
apt:
|
||||
state: "present"
|
||||
name: "fuse"
|
||||
|
||||
- name: "Create rclone Config Path"
|
||||
ansible.builtin.file:
|
||||
path: "/var/lib/docker-plugins/rclone/config"
|
||||
state: directory
|
||||
mode: "0750"
|
||||
|
||||
- name: "Create rclone Cache Path"
|
||||
ansible.builtin.file:
|
||||
path: "/var/lib/docker-plugins/rclone/cache"
|
||||
state: directory
|
||||
mode: "0750"
|
||||
|
||||
- name: "Disable the rclone Docker Plugin"
|
||||
community.docker.docker_plugin:
|
||||
state: "disable"
|
||||
alias: "rclone"
|
||||
plugin_name: "rclone/docker-volume-rclone:amd64"
|
||||
|
||||
- name: "Install rclone Docker Plugin"
|
||||
community.docker.docker_plugin:
|
||||
state: "present"
|
||||
alias: "rclone"
|
||||
plugin_name: "rclone/docker-volume-rclone:amd64"
|
||||
plugin_options:
|
||||
args: "-v --allow-other"
|
||||
|
||||
- name: "Enable the rclone Docker Plugin"
|
||||
community.docker.docker_plugin:
|
||||
state: "enable"
|
||||
alias: "rclone"
|
||||
plugin_name: "rclone/docker-volume-rclone:amd64"
|
||||
plugin_options:
|
||||
args: "-v --allow-other"
|
||||
|
||||
####################
|
||||
# - Docker - Swarm Init
|
||||
####################
|
||||
- hosts: leader
|
||||
become: "true"
|
||||
tasks:
|
||||
- name: "Initialize Docker Swarm Leader"
|
||||
community.docker.docker_swarm:
|
||||
state: "present"
|
||||
advertise_addr: "{{ wg0_ip }}"
|
||||
listen_addr: "{{ wg0_ip }}:2377"
|
||||
|
||||
- name: "Collect Swarm Info"
|
||||
community.docker.docker_swarm_info:
|
||||
register: swarm_info
|
||||
|
||||
- name: "Retrieve Join Tokens"
|
||||
set_fact:
|
||||
swarm_manager_token: "{{ swarm_info.swarm_facts['JoinTokens']['Manager'] }}"
|
||||
swarm_worker_token: "{{ swarm_info.swarm_facts['JoinTokens']['Worker'] }}"
|
||||
|
||||
- name: "Install jsondiff & pyyaml (stack-deploy deps)"
|
||||
apt:
|
||||
state: "present"
|
||||
name:
|
||||
- "python3-jsondiff"
|
||||
- "python3-yaml"
|
||||
|
||||
# SKIP Manager
|
||||
# - Currently, there is only one manager == leader. So there's no point.
|
||||
|
||||
- hosts: worker
|
||||
become: "true"
|
||||
tasks:
|
||||
- name: "Initialize Docker Swarm Workers"
|
||||
community.docker.docker_swarm:
|
||||
state: "join"
|
||||
advertise_addr: "{{ wg0_ip }}"
|
||||
join_token: "{{ hostvars[groups['leader'][0]]['swarm_worker_token'] }}"
|
||||
remote_addrs: [ "{{ hostvars[groups['leader'][0]]['wg0_ip'] }}:2377" ]
|
|
@ -0,0 +1,10 @@
|
|||
- hosts: swarm
|
||||
become: "true"
|
||||
tasks:
|
||||
####################
|
||||
# - Tuning - Dev
|
||||
####################
|
||||
- name: "Install Terminfo for Kitty"
|
||||
ansible.builtin.apt:
|
||||
state: "present"
|
||||
name: "kitty-terminfo"
|
|
@ -0,0 +1,43 @@
|
|||
- hosts: wg0
|
||||
become: "true"
|
||||
tasks:
|
||||
####################
|
||||
# - Wireguard
|
||||
####################
|
||||
- name: "Install Wireguard Tools"
|
||||
ansible.builtin.apt:
|
||||
state: "present"
|
||||
name: "wireguard"
|
||||
|
||||
- name: "systemd-networkd: Install wg0 Device"
|
||||
template:
|
||||
src: "./templates/99-wg0.netdev"
|
||||
dest: "/etc/systemd/network/99-wg0.netdev"
|
||||
owner: "root"
|
||||
group: "systemd-network"
|
||||
mode: "0640"
|
||||
|
||||
- name: "systemd-networkd: Install wg0 Network"
|
||||
template:
|
||||
src: "./templates/99-wg0.network"
|
||||
dest: "/etc/systemd/network/99-wg0.network"
|
||||
owner: "root"
|
||||
group: "systemd-network"
|
||||
mode: "0640"
|
||||
|
||||
- name: "Restart systemd-networkd"
|
||||
systemd:
|
||||
name: "systemd-networkd.service"
|
||||
state: "restarted"
|
||||
|
||||
####################
|
||||
# - Wireguard - Enable Packet Forwarding
|
||||
####################
|
||||
- name: "Set net.ipv4.ip_forward = 1"
|
||||
sysctl:
|
||||
state: "present"
|
||||
name: "net.ipv4.ip_forward"
|
||||
value: "1"
|
||||
reload: "yes"
|
||||
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
[NetDev]
|
||||
Name=wg0
|
||||
Kind=wireguard
|
||||
Description=WireGuard tunnel wg0
|
||||
|
||||
[WireGuard]
|
||||
ListenPort=51871
|
||||
PrivateKey={{ wg_private_key }}
|
||||
|
||||
{% for item in groups['wg0'] %}
|
||||
{% if item != inventory_hostname %}
|
||||
[WireGuardPeer]
|
||||
PublicKey={{ hostvars[item]['wg_public_key'] }}
|
||||
PresharedKey={{ hostvars[item]['wg_psk_' ~ inventory_hostname] }}
|
||||
AllowedIPs={{ hostvars[item]['wg0_ip'] }}/32
|
||||
Endpoint={{ item }}:51871
|
||||
|
||||
{% endif %}
|
||||
{% endfor %}
|
|
@ -0,0 +1,5 @@
|
|||
[Match]
|
||||
Name=wg0
|
||||
|
||||
[Network]
|
||||
Address={{ wg0_ip }}/24
|
|
@ -0,0 +1,155 @@
|
|||
#!/bin/bash
|
||||
set -e ## Exit if Problems
|
||||
set -u ## Fail on Undefined Variable
|
||||
|
||||
SCRIPT_PATH="$(dirname "$(readlink -f "$0")")"
|
||||
|
||||
####################
|
||||
# - Constants
|
||||
####################
|
||||
PLAYBOOKS_PATH="$SCRIPT_PATH/playbooks"
|
||||
|
||||
INVENTORY="$SCRIPT_PATH/inventory.yml"
|
||||
|
||||
PLAYBOOK_HOSTS="$PLAYBOOKS_PATH/playbook.hosts.yml"
|
||||
PLAYBOOK_WG0="$PLAYBOOKS_PATH/playbook.wg0.yml"
|
||||
PLAYBOOK_SWARM="$PLAYBOOKS_PATH/playbook.swarm.yml"
|
||||
|
||||
PLAYBOOK_STACK_CLEANUP="$SCRIPT_PATH/stacks/cleanup/playbook.yml"
|
||||
PLAYBOOK_STACK_MESH="$SCRIPT_PATH/stacks/mesh/playbook.yml"
|
||||
PLAYBOOK_STACK_SITE_SUPPORT="$SCRIPT_PATH/stacks/site-support/playbook.yml"
|
||||
|
||||
help() {
|
||||
less -R << EOF
|
||||
This script manages the deployment using ansible.
|
||||
|
||||
Usage:
|
||||
./run.sh [COMMAND]
|
||||
EOF
|
||||
}
|
||||
|
||||
####################
|
||||
# - Utilities
|
||||
####################
|
||||
cmd_exists() {
|
||||
if type -P "$1" &> /dev/null || [ -x "$1" ]; then
|
||||
echo true
|
||||
else
|
||||
echo false
|
||||
fi
|
||||
}
|
||||
|
||||
pkg_installed() {
|
||||
if [ $(dpkg-query -W -f='${Status}' "$1" 2>/dev/null | grep -c "ok installed") -eq 0 ]; then
|
||||
echo false
|
||||
else
|
||||
echo true
|
||||
fi
|
||||
}
|
||||
|
||||
####################
|
||||
# - Check Preconditions
|
||||
####################
|
||||
if [[ $(whoami) == root ]]; then
|
||||
echo "Please don't run as root."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
case $(cat /etc/debian_version | cut -d . -f 1) in
|
||||
"11")
|
||||
echo "Detected Debian 11 (Supported)..."
|
||||
;;
|
||||
"12")
|
||||
echo "Detected Debian 12 (Supported)..."
|
||||
;;
|
||||
*)
|
||||
echo "Could not detect a supported OS. Refer to manual for more."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ $(cmd_exists ansible) != true ]]; then
|
||||
echo "This script requires ansible. Press ENTER to install and continue..."
|
||||
sudo apt install ansible
|
||||
|
||||
echo "This script requires latest community.docker module. Press ENTER to install and continue..."
|
||||
ansible-galaxy collection install community.docker
|
||||
fi
|
||||
|
||||
####################
|
||||
# - Actions
|
||||
####################
|
||||
action_hosts() {
|
||||
ansible-playbook \
|
||||
--inventory "$INVENTORY" \
|
||||
"$PLAYBOOK_HOSTS"
|
||||
}
|
||||
action_wg0() {
|
||||
ansible-playbook \
|
||||
--inventory "$INVENTORY" \
|
||||
"$PLAYBOOK_WG0"
|
||||
}
|
||||
action_swarm() {
|
||||
ansible-playbook \
|
||||
--inventory "$INVENTORY" \
|
||||
"$PLAYBOOK_SWARM"
|
||||
}
|
||||
|
||||
action_stack_cleanup() {
|
||||
ansible-playbook \
|
||||
--inventory "$INVENTORY" \
|
||||
"$PLAYBOOK_STACK_CLEANUP"
|
||||
}
|
||||
action_stack_mesh() {
|
||||
ansible-playbook \
|
||||
--inventory "$INVENTORY" \
|
||||
"$PLAYBOOK_STACK_MESH"
|
||||
}
|
||||
action_stack_site_support() {
|
||||
ansible-playbook \
|
||||
--inventory "$INVENTORY" \
|
||||
"$PLAYBOOK_STACK_SITE_SUPPORT"
|
||||
}
|
||||
|
||||
####################
|
||||
# - Check Dependencies
|
||||
####################
|
||||
case $1 in
|
||||
sync)
|
||||
action_hosts
|
||||
action_wg0
|
||||
action_swarm
|
||||
|
||||
action_stack_cleanup
|
||||
action_stack_mesh
|
||||
action_stack_site_support
|
||||
;;
|
||||
|
||||
sync-hosts)
|
||||
action_hosts
|
||||
;;
|
||||
sync-wg0)
|
||||
action_wg0
|
||||
;;
|
||||
sync-swarm)
|
||||
action_swarm
|
||||
;;
|
||||
|
||||
sync-stack-cleanup)
|
||||
action_stack_cleanup
|
||||
;;
|
||||
sync-stack-mesh)
|
||||
action_stack_mesh
|
||||
;;
|
||||
sync-stack-site-support)
|
||||
action_stack_site_support
|
||||
;;
|
||||
|
||||
# sync-role)
|
||||
# ansible-playbook \
|
||||
# --inventory "$INVENTORY" \
|
||||
# --tags "$2" \
|
||||
# "$PLAYBOOK"
|
||||
# ;;
|
||||
|
||||
esac
|
|
@ -0,0 +1,6 @@
|
|||
auth
|
||||
chat
|
||||
git
|
||||
s3
|
||||
updater
|
||||
uptime
|
|
@ -0,0 +1,2 @@
|
|||
# TODO
|
||||
- [ ] Security
|
|
@ -0,0 +1,31 @@
|
|||
# Security
|
||||
Here follows an explanation of security practices taken into account.
|
||||
|
||||
Refer to https://docs.docker.com/compose/compose-file/compose-file-v3/ for explanations of individual points.
|
||||
|
||||
## Rootness
|
||||
**The container process runs as `root`**.
|
||||
|
||||
Due to the deterministic, static nature of the container process, this is not an issue.
|
||||
|
||||
## Port Exposure
|
||||
The container exposes no ports.
|
||||
|
||||
## Volume Access
|
||||
**The container process has `docker.sock` access**.
|
||||
|
||||
Due to the deterministic, static nature of the container process, this is not an issue.
|
||||
|
||||
## Resource Limits
|
||||
The service employs CPU/Memory usage limits in the `deploy` section.
|
||||
|
||||
This helps prevent any issues with the container process from crashing the entire host.
|
||||
|
||||
## Capabilities
|
||||
All capabilities are dropped with `--cap_drop ALL`.
|
||||
|
||||
No capabilities need to be added back, so none are.
|
||||
|
||||
## Special Note: latest
|
||||
Hosts are presumed to be kept up-to-date via the official `docker-ce` package.
|
||||
Thus, uniquely, using `latest` tag in this container is warranted.
|
|
@ -0,0 +1,37 @@
|
|||
version: "3.8"
|
||||
|
||||
services:
|
||||
docker-cleanup:
|
||||
image: docker.io/docker:latest
|
||||
cap_drop:
|
||||
- ALL
|
||||
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
|
||||
entrypoint: []
|
||||
command:
|
||||
- "sh"
|
||||
- "-euc"
|
||||
- |
|
||||
while true; do
|
||||
docker image prune --all --force
|
||||
docker system prune --all --force --volumes
|
||||
|
||||
sleep 86400
|
||||
## 1 day, in seconds
|
||||
done
|
||||
|
||||
deploy:
|
||||
mode: global
|
||||
|
||||
resources:
|
||||
limits:
|
||||
cpus: "1.0"
|
||||
memory: "1G"
|
||||
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
delay: 5s
|
||||
max_attempts: 3
|
||||
window: 120s
|
|
@ -0,0 +1,28 @@
|
|||
- hosts: leader
|
||||
become: "true"
|
||||
vars:
|
||||
stack_name: "cleanup"
|
||||
tasks:
|
||||
####################
|
||||
# - Stack Deployment
|
||||
####################
|
||||
- name: "Upload Stack to /tmp"
|
||||
template:
|
||||
src: "./docker-compose.yml"
|
||||
dest: "/tmp/{{ stack_name }}.yml"
|
||||
owner: "root"
|
||||
group: "root"
|
||||
mode: "0640"
|
||||
|
||||
- name: "Deploy Stack: {{ stack_name }}"
|
||||
community.docker.docker_stack:
|
||||
state: "present"
|
||||
prune: "true"
|
||||
name: "{{ stack_name }}"
|
||||
compose:
|
||||
- "/tmp/{{ stack_name }}.yml"
|
||||
|
||||
- name: "Delete /tmp Stack"
|
||||
ansible.builtin.file:
|
||||
path: "/tmp/{{ stack_name }}.yml"
|
||||
state: "absent"
|
|
@ -0,0 +1,2 @@
|
|||
# TODO
|
||||
- [ ] Configure Services per-Stack
|
|
@ -0,0 +1,10 @@
|
|||
[http.routers.site-support__site-support]
|
||||
rule = "Host(`pysupport.timesigned.com`)"
|
||||
entryPoints = ["websecure", "web"]
|
||||
service = "site-support__site-support"
|
||||
|
||||
[[http.services.site-support__site-support.loadBalancer.servers]]
|
||||
url = "http://site-support:8787"
|
||||
|
||||
[http.routers.site-support__site-support.tls]
|
||||
certResolver = "letsencrypt"
|
|
@ -0,0 +1,21 @@
|
|||
####################
|
||||
# - Default Middlewares
|
||||
####################
|
||||
[http.middlewares.default.chain]
|
||||
middlewares = [
|
||||
"default-security-headers",
|
||||
]
|
||||
|
||||
####################
|
||||
# - Middleware: Default Security Headers
|
||||
####################
|
||||
[http.middlewares.default-security-headers.headers]
|
||||
browserXssFilter = true # X-XSS-Protection=1; mode=block
|
||||
contentTypeNosniff = true # X-Content-Type-Options=nosniff
|
||||
forceSTSHeader = true # Add STS even when using HTTP.
|
||||
frameDeny = true # X-Frame-Options=deny
|
||||
referrerPolicy = "strict-origin-when-cross-origin"
|
||||
sslRedirect = true # Allow only https requests
|
||||
stsIncludeSubdomains = true # Add includeSubdomains to STS header
|
||||
stsPreload = true # Add preload flag appended to STS header
|
||||
stsSeconds = 63072000 # Set max-age of STS header (2 years)
|
|
@ -0,0 +1,63 @@
|
|||
####################
|
||||
# - Global Config
|
||||
####################
|
||||
[global]
|
||||
checkNewVersion = false
|
||||
sendAnonymousUsage = false
|
||||
|
||||
[experimental]
|
||||
http3 = true
|
||||
|
||||
[api]
|
||||
dashboard = false
|
||||
insecure = false
|
||||
debug = false
|
||||
disabledashboardad = true
|
||||
|
||||
[log]
|
||||
level = "DEBUG"
|
||||
|
||||
|
||||
|
||||
####################
|
||||
# - Certificate Resolvers
|
||||
# * https://doc.traefik.io/traefik/https/acme/#certificate-resolvers
|
||||
####################
|
||||
[certificatesResolvers.letsencrypt.acme]
|
||||
email = "{{ email_letsencrypt }}"
|
||||
storage = "/data-certs/acme.json"
|
||||
#caServer = "https://acme-staging-v02.api.letsencrypt.org/directory"
|
||||
|
||||
[certificatesResolvers.letsencrypt.acme.tlsChallenge]
|
||||
|
||||
|
||||
|
||||
####################
|
||||
# - Entry Points
|
||||
####################
|
||||
#[entryPoints.ssh]
|
||||
#address = ":22"
|
||||
|
||||
|
||||
[entryPoints.websecure]
|
||||
address = ":443"
|
||||
http3.advertisedPort = 443
|
||||
|
||||
|
||||
[entryPoints.web]
|
||||
address = ":80"
|
||||
|
||||
[entryPoints.web.http.redirections.entryPoint]
|
||||
to = "websecure"
|
||||
scheme = "https"
|
||||
permanent = true
|
||||
|
||||
|
||||
|
||||
####################
|
||||
# - Providers
|
||||
####################
|
||||
[providers.file]
|
||||
directory = "/data-providers"
|
||||
watch = false
|
||||
debugLogGeneratedTemplate = true
|
|
@ -0,0 +1,16 @@
|
|||
####################
|
||||
# - TLS Defaults
|
||||
# * See https://doc.traefik.io/traefik/https/tls/
|
||||
# * Adapted from https://ssl-config.mozilla.org
|
||||
####################
|
||||
[tls.options.default]
|
||||
minVersion = "VersionTLS12"
|
||||
sniStrict = true
|
||||
cipherSuites = [
|
||||
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
|
||||
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
|
||||
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
|
||||
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
|
||||
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305",
|
||||
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305"
|
||||
]
|
|
@ -0,0 +1,89 @@
|
|||
version: "3.8"
|
||||
|
||||
services:
|
||||
traefik:
|
||||
image: traefik:v2.10
|
||||
user: "5000:5000"
|
||||
|
||||
configs:
|
||||
- source: mesh__traefik_static.toml
|
||||
target: /etc/traefik/traefik.toml
|
||||
uid: "5000"
|
||||
gid: "5000"
|
||||
|
||||
- source: mesh__traefik_tls.toml
|
||||
target: /etc/traefik/dynamic/tls.toml
|
||||
uid: "5000"
|
||||
gid: "5000"
|
||||
|
||||
- source: mesh__traefik_default_middlewares.toml
|
||||
target: /etc/traefik/dynamic/default_middlewares.toml
|
||||
uid: "5000"
|
||||
gid: "5000"
|
||||
|
||||
- source: mesh__stack_site-support.toml
|
||||
target: /data-providers/site-support.toml
|
||||
uid: "5000"
|
||||
gid: "5000"
|
||||
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
|
||||
- mesh__traefik_certs:/data-certs
|
||||
|
||||
ports:
|
||||
## HTTP
|
||||
- target: 80
|
||||
published: 80
|
||||
protocol: tcp
|
||||
mode: host
|
||||
|
||||
## HTTPS
|
||||
- target: 443
|
||||
published: 443
|
||||
protocol: tcp
|
||||
mode: host
|
||||
|
||||
deploy:
|
||||
mode: replicated
|
||||
replicas: 1
|
||||
|
||||
update_config:
|
||||
parallelism: 1
|
||||
delay: 10s
|
||||
order: stop-first
|
||||
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
delay: 10s
|
||||
max_attempts: 3
|
||||
window: 120s
|
||||
|
||||
placement:
|
||||
constraints:
|
||||
- node.role == manager
|
||||
|
||||
networks:
|
||||
- mesh_public
|
||||
|
||||
####################
|
||||
# - Resources
|
||||
####################
|
||||
configs:
|
||||
mesh__traefik_static.toml:
|
||||
external: true
|
||||
mesh__traefik_tls.toml:
|
||||
external: true
|
||||
mesh__traefik_default_middlewares.toml:
|
||||
external: true
|
||||
mesh__stack_site-support.toml:
|
||||
external: true
|
||||
|
||||
volumes:
|
||||
mesh__traefik_certs:
|
||||
external: true
|
||||
|
||||
networks:
|
||||
mesh_public:
|
||||
external: true
|
|
@ -0,0 +1,120 @@
|
|||
####################
|
||||
# - Stop the Stack
|
||||
####################
|
||||
- hosts: leader
|
||||
become: "true"
|
||||
vars:
|
||||
stack_name: "mesh"
|
||||
tasks:
|
||||
- name: "Stop Stack: {{ stack_name }}"
|
||||
community.docker.docker_stack:
|
||||
state: "absent"
|
||||
absent_retries: 15
|
||||
name: "{{ stack_name }}"
|
||||
|
||||
- name: "Pause to Let Stack Stop"
|
||||
pause:
|
||||
seconds: 5
|
||||
|
||||
|
||||
####################
|
||||
# - Volume Creation
|
||||
####################
|
||||
- hosts: swarm
|
||||
become: "true"
|
||||
vars:
|
||||
cloudflare_b0__access_key_id: "{{ lookup('community.general.passwordstore', 'work/dtu/python-support/r2/mesh__traefik_certs/access_key_id') }}"
|
||||
cloudflare_b0__secret_access_key: "{{ lookup('community.general.passwordstore', 'work/dtu/python-support/r2/mesh__traefik_certs/secret_access_key') }}"
|
||||
cloudflare_b0__endpoint: "{{ lookup('community.general.passwordstore', 'work/dtu/python-support/r2/mesh__traefik_certs/endpoint') }}"
|
||||
|
||||
tasks:
|
||||
- name: "Unmount Volume: mesh__traefik_certs"
|
||||
community.docker.docker_volume:
|
||||
state: "absent"
|
||||
name: "mesh__traefik_certs"
|
||||
driver: "rclone"
|
||||
|
||||
- name: "Pause to Let Volume Unmount"
|
||||
pause:
|
||||
seconds: 5
|
||||
|
||||
- name: "Mount Volume: mesh__traefik_certs"
|
||||
community.docker.docker_volume:
|
||||
state: "present"
|
||||
name: "mesh__traefik_certs"
|
||||
driver: "rclone"
|
||||
driver_options:
|
||||
remote: ":s3:mesh--traefik-certs"
|
||||
uid: "5000"
|
||||
gid: "5000"
|
||||
s3_provider: "Cloudflare"
|
||||
s3_access_key_id: "{{ cloudflare_b0__access_key_id }}"
|
||||
s3_secret_access_key: "{{ cloudflare_b0__secret_access_key }}"
|
||||
s3_region: "auto"
|
||||
s3_endpoint: "{{ cloudflare_b0__endpoint }}"
|
||||
s3_acl: "private"
|
||||
vfs_cache_mode: "full"
|
||||
|
||||
####################
|
||||
# - Deployment
|
||||
####################
|
||||
- hosts: leader
|
||||
become: "true"
|
||||
vars:
|
||||
email_letsencrypt: "s174509@dtu.dk"
|
||||
|
||||
stack_name: "mesh"
|
||||
stack_configs:
|
||||
- "mesh__traefik_static.toml"
|
||||
- "mesh__traefik_tls.toml"
|
||||
- "mesh__traefik_default_middlewares.toml"
|
||||
- "mesh__stack_site-support.toml"
|
||||
|
||||
tasks:
|
||||
####################
|
||||
# - Network Creation
|
||||
####################
|
||||
- name: "Create Network: mesh_public"
|
||||
community.docker.docker_network:
|
||||
state: "present"
|
||||
name: "mesh_public"
|
||||
driver: "overlay"
|
||||
scope: "swarm"
|
||||
attachable: true
|
||||
appends: true
|
||||
|
||||
|
||||
####################
|
||||
# - Configs Creation
|
||||
####################
|
||||
- name: "Create Docker Configs"
|
||||
community.docker.docker_config:
|
||||
state: "present"
|
||||
name: "{{ item }}"
|
||||
data: "{{ lookup('template', './configs/' ~ item) | b64encode }}"
|
||||
data_is_b64: "true"
|
||||
with_items: "{{ stack_configs }}"
|
||||
|
||||
####################
|
||||
# - Stack Deployment
|
||||
####################
|
||||
- name: "Upload Stack to /tmp"
|
||||
template:
|
||||
src: "./docker-compose.yml"
|
||||
dest: "/tmp/{{ stack_name }}.yml"
|
||||
owner: "root"
|
||||
group: "root"
|
||||
mode: "0640"
|
||||
|
||||
- name: "Deploy Stack: {{ stack_name }}"
|
||||
community.docker.docker_stack:
|
||||
state: "present"
|
||||
prune: "true"
|
||||
name: "{{ stack_name }}"
|
||||
compose:
|
||||
- "/tmp/{{ stack_name }}.yml"
|
||||
|
||||
- name: "Delete /tmp Stack"
|
||||
ansible.builtin.file:
|
||||
path: "/tmp/{{ stack_name }}.yml"
|
||||
state: "absent"
|
|
@ -0,0 +1,17 @@
|
|||
# TODO
|
||||
- [ ] Test
|
||||
|
||||
|
||||
|
||||
# Introduction
|
||||
This is stack deploys the `python-support` website.
|
||||
|
||||
|
||||
|
||||
# Monitoring
|
||||
Check for:
|
||||
- Expired security.txt
|
||||
|
||||
Consider checking for:
|
||||
- Revoked security.txt key
|
||||
- Tag update (with alert to the server webhook)
|
|
@ -0,0 +1,47 @@
|
|||
# Security
|
||||
Here follows an explanation of security practices taken into account.
|
||||
|
||||
Refer to https://docs.docker.com/compose/compose-file/compose-file-v3/ for explanations of individual points.
|
||||
|
||||
## Rootness
|
||||
The container process runs as `5000:5000`.
|
||||
No processes are run as root within the container.
|
||||
|
||||
## Port Exposure
|
||||
The container participates in the private `mesh_public` overlay network.
|
||||
This allows the reverse proxy, Traefik, to route traffic via. internal DNS.
|
||||
|
||||
This traffic is unencrypted HTTP.
|
||||
Thus, **the overlay network must be run on a trusted (L3) network**.
|
||||
|
||||
## Volume Access
|
||||
Only `localtime` and `timezone` are mounted (read-only).
|
||||
|
||||
All files to be served are either baked into the container image, or mounted with `docker config`.
|
||||
|
||||
## Resource Limits
|
||||
The service employs CPU/Memory usage limits in the `deploy` section.
|
||||
|
||||
This helps prevent a DDoS attack from crashing the entire host.
|
||||
|
||||
## Capabilities
|
||||
All capabilities are dropped with `--cap_drop ALL`.
|
||||
|
||||
No capabilities need to be added back, so none are.
|
||||
|
||||
## security.txt
|
||||
*See https://securitytxt.org/ for RFC + generator.*
|
||||
|
||||
This stack comes with a `security.txt` generator in `scripts__security_txt`, which:
|
||||
- Templates mail contact, expiry, GPG public key link, and canonical path.
|
||||
- Signs the file with the GPG private key referenced in the link.
|
||||
|
||||
To use it, first adjust the following block in `gen.py`:
|
||||
```python
|
||||
MAILTO =
|
||||
EXPIRY =
|
||||
MAILTO_PGP_FINGERPRINT =
|
||||
DEPLOY_DOMAIN =
|
||||
```
|
||||
|
||||
Then, run `./gen.py` from any working directory. Remember to review the generated file, and update `docker config`.
|
|
@ -0,0 +1,24 @@
|
|||
-----BEGIN PGP SIGNED MESSAGE-----
|
||||
Hash: SHA512
|
||||
|
||||
Contact: mailto:s174509@dtu.dk
|
||||
Expires: 2024-08-01T00:00:00
|
||||
Encryption: https://keys.openpgp.org/vks/v1/by-fingerprint/E3B345EFFF5B3994BC1D12603D01BE95F3EFFEB9
|
||||
Preferred-Languages: en, dk
|
||||
Canonical: https://timesigned.com/.well-known/security.txt
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
iQIzBAEBCgAdFiEEG10i+uTnDBwXTs3FrZAcsPNwFDQFAmTUpRcACgkQrZAcsPNw
|
||||
FDRt9g/9GnqvAVUCBEZYtv+WwizxRe1iZF5ABIHytnymqsgjNjoF0uBxCZzR7MFZ
|
||||
z7yP/ChmaS9g14DOSAUs5I3si3mF1pcHgS0/auGMB84xg2p3Jn1ZmUIU2mPppEqw
|
||||
PvIju6hM5dSEgZap8iwxUis7bIqdtV+PeYfZdzRkXyVnBSCNpbK9VHX5enyMX7MD
|
||||
Is7PzQorn3MwytmhxOkYZ4XRxFd2OUtMm8QDQuSZjPSCEtXykH5Y6ITn1nCuJYQw
|
||||
Nz9wyE4bNnzdZMVFWzDdwICDHoWzQO3SCvyDbxKlDnY+AN2/6pzKvPo+C3iMpNdo
|
||||
MG+BuXVKc2ZwOj4+g6Srk9sM0flMy83HHOTYFXLx2M7guaa/+WaJK7GiKjaQUQJk
|
||||
fV/toxLEpmZONbGFQQR9wXvwA6iIee08A2Le9gmGdD2T/OUrTOVXemqd9tvhfDPn
|
||||
RserBgHnnFO7+ucIFjtqwhMmh3iXLg+x/cZyvt25Gke9WhwPu9oEEMLmP/M2N7XC
|
||||
TGopbg7GbDoZNY/BEz0Fh49DNYef8kemFc/qEFBV0XbVZRqIH0+zBrrs6z9LdSy+
|
||||
soB4yooK7dBa3Sxx01jYwv6o5yaKcBbxeNIx3Xf8awLONspr5RMELOSPSECAERs+
|
||||
GHYcpHSvBMzrdaz+uW9tHgKUAK9URDO8DOQphltZpg1ldTFIrZA=
|
||||
=XXVW
|
||||
-----END PGP SIGNATURE-----
|
|
@ -0,0 +1,60 @@
|
|||
version: "3.8"
|
||||
|
||||
services:
|
||||
site-support:
|
||||
image: git.sofus.io/so-rose/site-support:0
|
||||
user: "5020:5020"
|
||||
cap_drop:
|
||||
- ALL
|
||||
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
|
||||
configs:
|
||||
- source: site-support__security.txt
|
||||
target: /public/.well-known/security.txt
|
||||
uid: "5020"
|
||||
gid: "5020"
|
||||
|
||||
environment:
|
||||
SERVER_PORT: "8787"
|
||||
SERVER_REDIRECT_TRAILING_SLASH: "true"
|
||||
|
||||
SERVER_LOG_LEVEL: "info"
|
||||
SERVER_LOG_REMOTE_ADDRESS: "false"
|
||||
|
||||
SERVER_THREADS_MULTIPLIER: "0" ## Use # CPUs
|
||||
|
||||
SERVER_SECURITY_HEADERS: "true"
|
||||
SERVER_DIRECTORY_LISTING: "false"
|
||||
|
||||
SERVER_CACHE_CONTROL_HEADERS: "false" ## change when stable?
|
||||
SERVER_COMPRESSION: "true" ## reconsider for small ssg payload
|
||||
SERVER_COMPRESSION_STATIC: "false" ## pre-compress? :)
|
||||
|
||||
deploy:
|
||||
mode: replicated
|
||||
replicas: 1
|
||||
|
||||
# resources:
|
||||
# limits:
|
||||
# cpus: "4.0"
|
||||
# memory: "4G"
|
||||
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
delay: 5s
|
||||
max_attempts: 3
|
||||
window: 120s
|
||||
|
||||
networks:
|
||||
- mesh_public
|
||||
|
||||
configs:
|
||||
site-support__security.txt:
|
||||
external: true
|
||||
|
||||
networks:
|
||||
mesh_public:
|
||||
external: true
|
|
@ -0,0 +1,66 @@
|
|||
####################
|
||||
# - Deployment
|
||||
####################
|
||||
- hosts: leader
|
||||
become: "true"
|
||||
vars:
|
||||
stack_name: "site-support"
|
||||
stack_configs:
|
||||
- "site-support__security.txt"
|
||||
|
||||
tasks:
|
||||
####################
|
||||
# - Stop the Stack
|
||||
####################
|
||||
- name: "Stop Stack: {{ stack_name }}"
|
||||
community.docker.docker_stack:
|
||||
state: "absent"
|
||||
absent_retries: 15
|
||||
name: "{{ stack_name }}"
|
||||
|
||||
####################
|
||||
# - Network Creation
|
||||
####################
|
||||
- name: "Create Network: mesh_public"
|
||||
community.docker.docker_network:
|
||||
state: "present"
|
||||
name: "mesh_public"
|
||||
driver: "overlay"
|
||||
scope: "swarm"
|
||||
attachable: true
|
||||
appends: true
|
||||
|
||||
####################
|
||||
# - Config Creation
|
||||
####################
|
||||
- name: "Create Docker Configs"
|
||||
community.docker.docker_config:
|
||||
state: "present"
|
||||
name: "{{ item }}"
|
||||
data: "{{ lookup('template', './configs/' ~ item) | b64encode }}"
|
||||
data_is_b64: "true"
|
||||
with_items: "{{ stack_configs }}"
|
||||
|
||||
####################
|
||||
# - Stack Deployment
|
||||
####################
|
||||
- name: "Upload Stack to /tmp"
|
||||
template:
|
||||
src: "./docker-compose.yml"
|
||||
dest: "/tmp/{{ stack_name }}.yml"
|
||||
owner: "root"
|
||||
group: "root"
|
||||
mode: "0640"
|
||||
|
||||
- name: "Deploy Stack: {{ stack_name }}"
|
||||
community.docker.docker_stack:
|
||||
state: "present"
|
||||
prune: "true"
|
||||
name: "{{ stack_name }}"
|
||||
compose:
|
||||
- "/tmp/{{ stack_name }}.yml"
|
||||
|
||||
- name: "Delete /tmp Stack"
|
||||
ansible.builtin.file:
|
||||
path: "/tmp/{{ stack_name }}.yml"
|
||||
state: "absent"
|
|
@ -0,0 +1,126 @@
|
|||
#!/usr/bin/python3
|
||||
# Copyright (C) 2023 Sofus Albert Høgsbro Rose
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""This script templates and signs a `security.txt` file.
|
||||
|
||||
Note that:
|
||||
- This script presumes that `gpg` is installed.
|
||||
- This script presumes that the private key of the configued fingerprint is available to use with `gpg --clearsign`.
|
||||
- The keyserver is hardcoded to `keys.openpgp.org`.
|
||||
|
||||
To use, first adjust the following configuration block:
|
||||
```python
|
||||
MAILTO =
|
||||
EXPIRY =
|
||||
MAILTO_PGP_FINGERPRINT =
|
||||
DEPLOY_DOMAIN =
|
||||
```
|
||||
|
||||
Then, just run `./gen.py`.
|
||||
|
||||
**REMEMBER TO REVIEW THE GENERATED FILE BEFORE DEPLOYMENT**.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
if not all([
|
||||
sys.version_info.major == 3,
|
||||
sys.version_info.minor in [9, 10, 11, 12, 13],
|
||||
]):
|
||||
sys.exit(1)
|
||||
|
||||
from pathlib import Path
|
||||
import platform
|
||||
import shutil
|
||||
import subprocess
|
||||
import contextlib
|
||||
from datetime import datetime
|
||||
from string import Template
|
||||
|
||||
####################
|
||||
# - Configuration
|
||||
####################
|
||||
MAILTO = "s174509@dtu.dk"
|
||||
EXPIRY = datetime(year = 2024, month = 8, day = 1).isoformat()
|
||||
MAILTO_PGP_FINGERPRINT = "E3B345EFFF5B3994BC1D12603D01BE95F3EFFEB9"
|
||||
DEPLOY_DOMAIN = "https://timesigned.com"
|
||||
|
||||
####################
|
||||
# - Constants
|
||||
####################
|
||||
SCRIPT_PATH = Path(__file__).resolve().parent
|
||||
PATH_SECURITY_TXT = (
|
||||
SCRIPT_PATH.parent / "configs" / "site-support__security.txt"
|
||||
)
|
||||
|
||||
####################
|
||||
# - Utilities
|
||||
####################
|
||||
@contextlib.contextmanager
|
||||
def cd_script_dir() -> None:
|
||||
cwd_orig = Path.cwd()
|
||||
|
||||
os.chdir(SCRIPT_PATH)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
os.chdir(cwd_orig)
|
||||
|
||||
####################
|
||||
# - Actions
|
||||
####################
|
||||
def sign_security_txt() -> None:
|
||||
if PATH_SECURITY_TXT.is_file():
|
||||
PATH_SECURITY_TXT.unlink()
|
||||
## Avoid platform-defined (os.rename()) shutil.move() to existing file.
|
||||
|
||||
with cd_script_dir():
|
||||
# Template
|
||||
with open("security.txt.unsigned.tmpl", "r") as f0:
|
||||
with open("security.txt.unsigned", "w") as f1:
|
||||
f1.write(
|
||||
Template(
|
||||
f0.read()
|
||||
).substitute(
|
||||
MAILTO = MAILTO,
|
||||
EXPIRY = EXPIRY,
|
||||
MAILTO_PGP_FINGERPRINT = MAILTO_PGP_FINGERPRINT,
|
||||
DEPLOY_DOMAIN = DEPLOY_DOMAIN,
|
||||
)
|
||||
)
|
||||
|
||||
# Sign + Delete Templated
|
||||
subprocess.run([
|
||||
"gpg",
|
||||
"--local-user", "E3B345EFFF5B3994BC1D12603D01BE95F3EFFEB9",
|
||||
"--clearsign", "security.txt.unsigned",
|
||||
])
|
||||
Path("security.txt.unsigned").unlink()
|
||||
|
||||
# Move
|
||||
shutil.move(
|
||||
"security.txt.unsigned.asc",
|
||||
PATH_SECURITY_TXT,
|
||||
)
|
||||
|
||||
####################
|
||||
# - Main
|
||||
####################
|
||||
if __name__ == "__main__":
|
||||
sign_security_txt()
|
||||
|
||||
# `cat` the Installed File
|
||||
with open(PATH_SECURITY_TXT, "r") as f:
|
||||
print(f.read(), end = "")
|
|
@ -0,0 +1,5 @@
|
|||
Contact: mailto:$MAILTO
|
||||
Expires: $EXPIRY
|
||||
Encryption: https://keys.openpgp.org/vks/v1/by-fingerprint/$MAILTO_PGP_FINGERPRINT
|
||||
Preferred-Languages: en, dk
|
||||
Canonical: $DEPLOY_DOMAIN/.well-known/security.txt
|
Loading…
Reference in New Issue