Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
artur-sannikov committed Aug 22, 2024
0 parents commit 73cd3ef
Show file tree
Hide file tree
Showing 17 changed files with 643 additions and 0 deletions.
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
inventory
Caddyfile
ansible.cfg
host_vars/*.yml
roles/reverse_proxy/files/caddy_override.conf

.vscode/
public_keys/
61 changes: 61 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Caddy reverse proxy setup with Crowdsec plugin

This Ansible repo sets up a Caddy reverse proxy for my homelab services. It integrates with the Crowdsec plugin that runs on my OPNsense box.

## How?

Dustin Casto wrote an outstanding guide on this setup on [his website](https://homenetworkguy.com/how-to/set-up-caddy-reverse-proxy-with-lets-encrypt-and-crowdsec-using-opnsense-lapi/). I automated it with Ansible.

Caddy runs on a VM in [Proxmox](https://www.proxmox.com/en/) on a DMZ network. I access it via [Tailscale](https://tailscale.com/), and it redirects me to the desired service.

Every service runs with a valid HTTPS certificate. I use a wildcard certificate because it's easier to manage. I also found that retrieving certificates for each subdomain is very unreliable on my network and might take hours.

I also do some SSH hardening and set up a UFW firewall.

## Prerequisites

1. Cloudflare account with an API key with permissions: `Zone.Zone Read` and `Zone.DNS Edit`. You need to set up this key `roles/reverse_proxy/files/caddy_override.conf` file
2. I use OPNsense as my firewall. However, t should be possible to modify this playbook to just install and set up Caddy.
3. A domain name. Since we want valid HTTPS certificates.
4. Ubuntu. This set-up has been tested on Ubuntu 24.04, but should work on any Debian-based system.
5. Public key for SSH authentication is set in the root of the repository in the `files/ansible.pub` file.

### Set up Crowdsec on OPNsense

This repo assumes that you have set up Crowdsec on OPNsense according to [Dustin's guide](https://homenetworkguy.com/how-to/set-up-caddy-reverse-proxy-with-lets-encrypt-and-crowdsec-using-opnsense-lapi/). During the playbook's run, it will pause and ask you to validate the Caddy machine in OPNsense. Make sure you do it as described in the guide.

### Set up Cloudflare

This repo builds Caddy with the Cloudflare plugin to perform the DNS-01 challenge to validate that you own a domain. [This guide](https://homenetworkguy.com/how-to/replace-opnsense-web-ui-self-signed-certificate-with-lets-encrypt/) might be helpful

## Variables and files

Define your host variables in `host_vars/caddyDMZ.yml`. The comments in the example file should be helpful.

Set up your [Caddyfile](https://caddyserver.com/docs/caddyfile).

Set up `caddy_override.conf`. It only contais Cloudflare API token. Keep it **safe**!

Remove the `.example` suffix from the provided files.

## How to run?

1. Provided you have Ansible installed and `inventory` and `ansible.cfg` files created, you need to run `ansible-playbook bootsrap.yml`. It will create a specified Ansible user and give it `sudo` rights. From now on, you can run any Ansible playbook as this user.
2. Run `ansible-playbook caddy.yml`.

## Things to keep in mind

1. I use `unattended-upgrades` to upgrade the system. It also _might_ reboot if it's necessary after the update. That's not ideal if you want 100% availability and stability because things can break after an update. Ubuntu is a rather stable system, and I only automatically install security updates; it _should_ not break after a reboot.
2. I took steps to harden SSH and set up a firewall, but it is not a super-protected system. I do not expose any ports on my home network and use public key authentication for SSH, so this security level works for me.
3. If you are inside your home network, you can set up OPNsense to override IP address that are returned by DNS. Now, if you request `service.example.com`, you will not query a remote DNS server, and the domain will resolve immediately to your local IP. See [Unbound DNS Override Aliases in OPNsense](https://homenetworkguy.com/how-to/create-unbound-dns-override-aliases-in-opnsense/) for more information.

## What's missing?

This is still work-in-progress. I use [Tailscale](https://tailscale.com) to access my network. Currently, I install and set up Tailscale manually.

## 🙌 Thanks

1. Dustin Casto for the amazing guide, [Set Up a Caddy Reverse Proxy with Let's Encrypt and CrowdSec Using OPNsense LAPI](https://homenetworkguy.com/how-to/set-up-caddy-reverse-proxy-with-lets-encrypt-and-crowdsec-using-opnsense-lapi/), this repo is based.
2. Jay LaCroix's excellent [Ansible video series](https://www.youtube.com/playlist?list=PLqyUgadpThTL1guZCdGy7H8V4snPrpj8t).
3. Jim's Garage for [changing the way I create new VMs](https://youtu.be/Kv6-_--y5CM).
4. Software developers whose work made this repo possible.
34 changes: 34 additions & 0 deletions bootstrap.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
- hosts: all
become: true
pre_tasks:
- name: Update packages (Ubuntu)
tags: always
ansible.builtin.apt:
update_cache: true
upgrade: true

- hosts: all
become: true
tasks:
- name: Create ansbile user
tags: always
ansible.builtin.user:
name: "{{ ansible_user }}"
group: root
state: present
- name: Add SSH key for ansible user
tags: always
ansible.posix.authorized_key:
user: "{{ ansible_user }}"
key: "{{ item }}"
with_file:
public_keys/ansible.pub
- name: Add sudoers file for ansible user
tags: always
ansible.builtin.copy:
src: "{{ sudoers_ansible }}"
dest: "{{ sudoers_ansible_path }}"
owner: root
group: root
mode: "0440"
6 changes: 6 additions & 0 deletions caddy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
- hosts: caddy
become: true
roles:
- base
- reverse_proxy
1 change: 1 addition & 0 deletions files/sudoers_hedgehog
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
hedgehog ALL=(ALL) NOPASSWD: ALL
6 changes: 6 additions & 0 deletions host_vars/caddyDMZ.yml.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
ansible_user: # ansible user that will manage the server
sudoers_ansible: # sudoers file for ansible user to give sudo rights
sudoers_ansible_path: /etc/sudoers.d/<ansible_user> # destination path to place sudoers file
ssh_users: "ansible_user" # set users that allowed SSH access to server (at least ansible_user)
ssh_template_file: sshd_config_ubuntu.j2 # template for hardened sshd_config
lapi_endpoint: http://192.168.1.1:8080 # Local API of Crowdsec bouncer on OPNsense
3 changes: 3 additions & 0 deletions roles/base/files/20auto-upgrades
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
APT::Periodic::AutocleanInterval "7";
13 changes: 13 additions & 0 deletions roles/base/handlers/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
- name: restart_unattended_upgrades
ansible.builtin.service:
name: unattended-upgrades
state: restarted

- name: restart_ssh
ansible.builtin.service:
name: ssh
state: restarted

- name: reload_ufw
community.general.ufw:
state: reloaded
78 changes: 78 additions & 0 deletions roles/base/tasks/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
- name: Install essential packages
tags: always
ansible.builtin.apt:
name: "{{ item }}"
state: latest
loop:
- unattended-upgrades

- name: Enable unattended-upgrades autoclean
tags: always
ansible.builtin.copy:
src: 20auto-upgrades
dest: /etc/apt/apt.conf.d/20auto-upgrades
owner: root
group: root
notify: restart_unattended_upgrades

- name: Enable automatic reboot after upgrade without confirmation
ansible.builtin.lineinfile:
path: /etc/apt/apt.conf.d/50unattended-upgrades
regexp: ^//Unattended-Upgrade::Automatic-Reboot "false";
line: Unattended-Upgrade::Automatic-Reboot "true";
notify: restart_unattended_upgrades

- name: Set automatic reboot time
ansible.builtin.lineinfile:
path: /etc/apt/apt.conf.d/50unattended-upgrades
regexp: ^//Unattended-Upgrade::Automatic-Reboot-Time
line: Unattended-Upgrade::Automatic-Reboot-Time "05:00";
notify: restart_unattended_upgrades

- name: Generate sshd_config from template
tags: ssh
template:
src: "{{ ssh_template_file }}"
dest: /etc/ssh/sshd_config
owner: root
group: root
mode: "0644"
notify: restart_ssh

- name: Limit access to ssh port
tags: ufw
community.general.ufw:
rule: limit
src: 192.168.10.0/24
port: "22"
proto: tcp

- name: Allow all access to http port
tags: ufw
community.general.ufw:
rule: allow
port: "80"
proto: tcp

- name: Allow all access to https port
tags: ufw
community.general.ufw:
rule: allow
port: "443"
proto: tcp

- name: Deny other incoming traffic and enable UFW
tags: ufw
community.general.ufw:
state: enabled
policy: deny
direction: incoming

- name: Disable UFW IPv6
tags: ufw
ansible.builtin.lineinfile:
path: /etc/default/ufw
regexp: ^IPV6=
line: IPV6=no
notify:
- reload_ufw
146 changes: 146 additions & 0 deletions roles/base/templates/sshd_config_ubuntu.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@

# This is the sshd server system-wide configuration file. See
# sshd_config(5) for more information.

# This sshd was compiled with PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games

# The strategy used for options in the default sshd_config shipped with
# OpenSSH is to specify options with their default value where
# possible, but leave them commented. Uncommented options override the
# default value.

Include /etc/ssh/sshd_config.d/*.conf

AllowUsers {{ ssh_users }}

#Port 22
#AddressFamily any
#ListenAddress 0.0.0.0
#ListenAddress ::

HostKey /etc/ssh/ssh_host_rsa_key
HostKey /etc/ssh/ssh_host_ed25519_key
#HostKey /etc/ssh/ssh_host_ecdsa_key

# Ciphers and keying
#RekeyLimit default none

# Cryptography
{{ 'Ciphers ' ~ ssh_ciphers|join(',') }}
{{ 'MACs ' ~ ssh_macs|join(',') }}
{{ 'KexAlgorithms ' ~ ssh_kex|join(',') }}
{{ 'HostKeyAlgorithms ' ~ ssh_client_host_key_algorithms|join(',') }}
{{ 'PubkeyAcceptedAlgorithms ' ~ ssh_pubkey_accepted_algorithms|join(',') }}

# Logging
#SyslogFacility AUTH
#LogLevel INFO

# Authentication
# --------------
LoginGraceTime 30s
PermitRootLogin no
StrictModes yes
MaxAuthTries 3
MaxSessions 10
MaxStartups 10:30:60

IgnoreRhosts yes
IgnoreUserKnownHosts yes
HostbasedAuthentication no

AuthenticationMethods publickey

# To disable tunneled clear text passwords, change to no here!
PermitEmptyPasswords no
PasswordAuthentication no

#PubkeyAuthentication yes

# Kerberos options
KerberosAuthentication no
KerberosOrLocalPasswd no
KerberosTicketCleanup yes

# Network
# -------
TCPKeepAlive no
ClientAliveInterval 300
ClientAliveCountMax 3

# Disable tunneling
PermitTunnel no

AllowAgentForwarding no
AllowTcpForwarding no

GatewayPorts no
X11Forwarding no
X11UseLocalhost yes

# Miscellaneous
# -------

Compression no
PrintMotd no

# no default banner path
Banner false

# Expect .ssh/authorized_keys2 to be disregarded by default in future.
#AuthorizedKeysFile .ssh/authorized_keys .ssh/authorized_keys2

#AuthorizedPrincipalsFile none

#AuthorizedKeysCommand none
#AuthorizedKeysCommandUser nobody

# For this to work you will also need host keys in /etc/ssh/ssh_known_hosts
# Change to yes if you don't trust ~/.ssh/known_hosts for
# HostbasedAuthentication
# Don't read the user's ~/.rhosts and ~/.shosts files


# Change to yes to enable challenge-response passwords (beware issues with
# some PAM modules and threads)
KbdInteractiveAuthentication no


# GSSAPI options
#GSSAPIAuthentication no
#GSSAPICleanupCredentials yes
#GSSAPIStrictAcceptorCheck yes
#GSSAPIKeyExchange no

# Set this to 'yes' to enable PAM authentication, account processing,
# and session processing. If this is enabled, PAM authentication will
# be allowed through the KbdInteractiveAuthentication and
# PasswordAuthentication. Depending on your PAM configuration,
# PAM authentication via KbdInteractiveAuthentication may bypass
# the setting of "PermitRootLogin prohibit-password".
# If you just want the PAM account and session checks to run without
# PAM authentication, then enable this but set PasswordAuthentication
# and KbdInteractiveAuthentication to 'no'.
UsePAM yes

#X11DisplayOffset 10
#PermitTTY yes
#PrintLastLog yes
#PermitUserEnvironment no
#PidFile /run/sshd.pid
#PermitTunnel no
#ChrootDirectory none
#VersionAddendum none

# Allow client to pass locale environment variables
AcceptEnv LANG LC_*

# override default of no subsystems
Subsystem sftp /usr/lib/openssh/sftp-server

# Example of overriding settings on a per-user basis
#Match User anoncvs
# X11Forwarding no
# AllowTcpForwarding no
# PermitTTY no
# ForceCommand cvs server
Loading

0 comments on commit 73cd3ef

Please sign in to comment.