Skip to content

Commit

Permalink
Add restic backups with OpenVPN
Browse files Browse the repository at this point in the history
* restic backups to local and remote repositories
  * wrapper script to create local backups and copy them to remote repositories
  * SSH setup for NAS to NAS access
* OpenVPN setup for NAS to NAS connection
  * rootless
  * automatic reconnection
  * helper script and documentation for CA setup and certificate flow
* Makefile to simplify playbook usage and OpenVPN setup
* various improvements
  * rootless miniDLNA
  * ddclient for DynV6 dynamic IP service
  * split vaults into machine-specific and global
  • Loading branch information
sebschlicht committed Aug 26, 2023
1 parent 7b51c55 commit b8dd7ef
Show file tree
Hide file tree
Showing 62 changed files with 1,925 additions and 391 deletions.
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
/*.retry
/.vault_pass

# default certificate authority location
/ca/
# files in transit to/from the certificate authority
/files-ca-transit/
# sensitive files distributed to OpenVPN clients
/files/*/openvpn/ta.key

/wiki
76 changes: 76 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
ANSIBLE_VAULT_PASSWORD_FILE = .vault_pass
ANSIBLE_INVENTORY = ko
EASYRSA_DIR = ca
EASYRSA_PKI_DIR = ${EASYRSA_DIR}/pki
CA_TRANSFER_FILES_DIR = files-ca-transit
REQUESTS_ARCHIVE = ${CA_TRANSFER_FILES_DIR}/requests.zip
CERTS_ARCHIVE = ${CA_TRANSFER_FILES_DIR}/certificates.zip

koblenz:
@$(eval ANSIBLE_INVENTORY=ko)

bendorf:
@$(eval ANSIBLE_INVENTORY=bendorf)

openvpn-initialize:
ansible-playbook --vault-password-file=${ANSIBLE_VAULT_PASSWORD_FILE} -i inventories/${ANSIBLE_INVENTORY} setup.yml --tags openvpn-init

openvpn-pack-requests:
@./openvpn-helper.sh pack-requests

openvpn-distribute-server-files:
@./openvpn-helper.sh copy-files "${ANSIBLE_VAULT_PASSWORD_FILE}"

openvpn-extract-certificates:
@[ -f "${CERTS_ARCHIVE}" ] || ( echo 'Certificates archive path invalid or unknown, use `make openvpn-extract-certificates CERTS_ARCHIVE=/path/to/certificates.zip`'; exit 2 )
@./openvpn-helper.sh extract-certificates "${CERTS_ARCHIVE}"

openvpn-configure:
ansible-playbook --vault-password-file=${ANSIBLE_VAULT_PASSWORD_FILE} -i inventories/${ANSIBLE_INVENTORY} setup.yml --tags openvpn-config

# for usage on a secure, offline CA device
openvpn-ca-init: export EASYRSA_PKI = ${EASYRSA_PKI_DIR}
openvpn-ca-init:
@echo '[WARN] Ensure to have your certificate authority reside on an offline device, due to security!'
@[ ! -f "${EASYRSA_PKI}/ca.crt" ] || ( echo 'CA is already fully initialized'; exit 3 )
@dpkg -s easy-rsa | grep Status | grep -q installed || sudo apt install -y easy-rsa
make-cadir "${EASYRSA_DIR}"
"${EASYRSA_DIR}/easyrsa" init-pki
@dd if=/dev/urandom of="${EASYRSA_PKI}/.rnd" bs=256 count=1
"${EASYRSA_DIR}/easyrsa" build-ca
openvpn-ca-sign-requests:
@[ -f "${REQUESTS_ARCHIVE}" ] || ( echo 'Requests archive path unknown, use `make openvpn-ca-sign-requests REQUESTS_ARCHIVE=/path/to/requests.zip`'; exit 2 )
@./openvpn-helper.sh sign-requests "${REQUESTS_ARCHIVE}"

setup-all:
ansible-playbook --vault-password-file=${ANSIBLE_VAULT_PASSWORD_FILE} -i inventories/${ANSIBLE_INVENTORY} setup.yml

setup-base-without-mount:
ansible-playbook --vault-password-file=${ANSIBLE_VAULT_PASSWORD_FILE} -i inventories/${ANSIBLE_INVENTORY} setup.yml --tags os,sshd,ufw,userdata

setup-base-with-mount:
ansible-playbook --vault-password-file=${ANSIBLE_VAULT_PASSWORD_FILE} -i inventories/${ANSIBLE_INVENTORY} setup.yml --tags os,sshd,ufw,mount,userdata

setup-auto-upgrade:
ansible-playbook --vault-password-file=${ANSIBLE_VAULT_PASSWORD_FILE} -i inventories/${ANSIBLE_INVENTORY} setup.yml --tags auto-upgrade

setup-backup:
ansible-playbook --vault-password-file=${ANSIBLE_VAULT_PASSWORD_FILE} -i inventories/${ANSIBLE_INVENTORY} setup.yml --tags backup

setup-dynv6:
ansible-playbook --vault-password-file=${ANSIBLE_VAULT_PASSWORD_FILE} -i inventories/${ANSIBLE_INVENTORY} setup.yml --tags ddns

setup-minidlna:
ansible-playbook --vault-password-file=${ANSIBLE_VAULT_PASSWORD_FILE} -i inventories/${ANSIBLE_INVENTORY} setup.yml --tags dlna

setup-mumble:
ansible-playbook --vault-password-file=${ANSIBLE_VAULT_PASSWORD_FILE} -i inventories/${ANSIBLE_INVENTORY} setup.yml --tags mumble

setup-samba:
ansible-playbook --vault-password-file=${ANSIBLE_VAULT_PASSWORD_FILE} -i inventories/${ANSIBLE_INVENTORY} setup.yml --tags samba

vault-edit-global-nas:
ansible-vault edit --vault-password-file=${ANSIBLE_VAULT_PASSWORD_FILE} group_vars/nas/vault

vault-edit-inventory-nas:
ansible-vault edit --vault-password-file=${ANSIBLE_VAULT_PASSWORD_FILE} inventories/${ANSIBLE_INVENTORY}/group_vars/nas/vault
158 changes: 83 additions & 75 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,135 +1,143 @@
# NAS Playbook

This repository contains Ansible playbooks and roles to transform your RaspberryPi (or another system based on Debian/Ubuntu) into a NAS.
At the very heart, the term NAS refers to an OpenSSH server to securely backup user data to:
At the very heart, the term NAS refers to a machine with:

* secure OpenSSH server to automatically backup files
* a secure OpenSSH server to automatically backup files
* encrypted file transfer (SSH)
* key-based authentication (e.g. [backupnas](https://github.com/sebschlicht/backupnas) for UNIX clients, [FreeFileSync](https://freefilesync.org/) for Windows clients)
* with all users created automatically and their SSH keys in place
* with `fail2ban` enabled
* with all users created and configured automatically
* `fail2ban` enabled

However, this playbook supports a variety of services (e.g. Samba, DLNA, Nextcloud) that can be plugged in by specifying one or more [tags](#tags).

While most configuration options are hard-coded to fit the recommendations for the respective service, you can configure (among others):

* the users, their SSH keys and passwords (individually for all services)
* the storage locations (for SSH-based backups and the Nextcloud instance)
* which folders to expose via DLNA
* the mail account to use for notifications
* users, their SSH keys and passwords
* storage locations
* folders accessible via DLNA
* mail account used for system notifications

## Tags

* `mount`: automatically mount an external hard drive to extent storage capabilities
* `mount`: automatically mount external hard drives to store user data/backups at
* `backup`: schedule [restic](https://restic.net/) to securely backup the entire user data (at 2 a.m.) to a local directory / external hard drive or remote location (SFTP)
* `cloud`: [Nextcloud](https://nextcloud.com/) server to upload and share files in a personal cloud
* encrypted file transfer (HTTPS)
* automatically renewed SSL certificates (Let's Encrypt)
* Mozilla's intermediate SSL configuration (secure but aiming for compatibility)
* fully installed and configured
* including all required components and users
* applying performance recommendations and hardening
* `samba`: [Samba](https://www.samba.org/) server to access stored files from any desktop or mobile device
* `samba`: [Samba](https://www.samba.org/) server to access stored files from any desktop or mobile device in your network
* password authentication for users, optional guest shares
* *unencrypted file transfer*
* `dlna`: [DLNA](https://en.wikipedia.org/wiki/Digital_Living_Network_Alliance) server to access stored media files from smartTVs etc.
* `dlna`: [DLNA](https://en.wikipedia.org/wiki/Digital_Living_Network_Alliance) server to access stored media files from a SmartTV etc.
* automatically discover and play videos and/or music
* *unencrypted file transfer*
* `mumble`: [Mumble](https://www.mumble.info/) server (aka. Murmur) for low-latency voice chats
* `ddns`: dynamic DNS update client
* hourly (and on reboot) reports the device's IP address to DynV6, to have it accessible via a domain
* `auto-upgrade`: keep installed software up-to-date
* `ufw`: firewall to restrict incoming traffic to the selected services (i.e. specified tags)
* `openvpn-config`: make the device an [OpenVPN client/server](#openvpn-setup) to form a network with another device

## Usage

In order to apply this playbook to one of your machines, there are basically two steps required:

1. [configure the inventory](#inventory-configuration) to your needs
1. Run the playbook:
1. Use the named [tags](#tags) to precisely choose which services to install, e.g. only Samba and Nextcloud:
2. Run the playbook, using [tags](#tags) to precisely choose which services to install:

ansible-playbook -i inventories/custom setup.yml --tags samba,nextcloud
ansible-playbook -i inventories/example setup.yml --tags samba,nextcloud

1. Omit tags to install all available services:
## Configuration

ansible-playbook -i inventories/custom setup.yml
The configuration consists of
1. global variables at `group_vars/all.yml`
2. an Ansible inventory containing
1. the address of the remote machine(s) at `hosts.yml`
2. specific variables at `group_vars/nas/vars.yml`
3. specific credentials in an [Ansible vault](https://docs.ansible.com/ansible/latest/user_guide/vault.html) at `group_vars/nas/vault`
3. files to be copied to the remote machine in the `files` folder

## Inventory Configuration
The example inventory at `inventories/example` can be copied and fully adapted to your needs.

The inventory `inventories/custom` is intended to be used when running this playbook and can be fully adapted to your needs.
The configuration variables are loosely structured by the features that this playbook offers.
If you use tags to selectively enable features, you only have to adapt their variables and may omit others.

Within this inventory, you *must* adapt the first two files and *should* make use of the third, though it's not technically required:
Check existing files for available variables and their meaning.

Path | Defines
----------------------------- | -------
`hosts.yml` | [hosts](#hosts) that the playbook is running against
`group_vars/custom/vars.yml` | [variables](#variables) that are used in the playbook
`group_vars/custom/vault.yml` | [vault](#vault) for credential variables
## Port Forwarding

### Hosts
> **_Note:_** Instead of forwarding relevant ports from your router to the NAS, strongly consider using a VPN to avoid any unencrypted file transfer (e.g. Samba) via internet.
The hosts file `hosts.yml` defines the machine that will be configured by this playbook.
The NAS runs different services that operate on various ports.
When operating the NAS behind a router (i.e. firewall), these ports have to be accessible.

Simply edit the file and place your machine's address/hostname (e.g. `pi3`):
In case you insist on using port forwarding, the following ports are used by the respective services:

all:
children:
nas:
hosts:
pi3
| Service | TCP | UDP |
| --------- | -------- | -------- |
| Samba | 139, 445 | 137, 138 |
| OpenSSH | 22 |
| Nextcloud | 80, 443 |

### Variables
These are the default ports.
Strongly-consider to expose the services at non-default ports to prevent the load and risk caused by bots.
An execption are the ports 80 and 443, as they are required for the automatic certificate renewal.

All variables of this playbook that have to be customized are defined in the YAML file `group_vars/custom/vars.yml`.
There are additional variables that usually do not need to be changed but are included in the table below.
## OpenVPN Setup

The variables are loosely structured by the different features that this playbook offers and should be quite self-explanatory already.
The table below lists which variables are available for each feature.
You only need to care about the variables of features that you are going to install.
This repository contains make goals (scripts) to setup a permanent OpenVPN connection between machines with internet access.
One of them acts as the server and thus must be reachable via internet (e.g. dynamic DNS).

| Role | Variable | Description
| ---- | -------- | -----------
| * | `setup_user` | remote user to login and use for the setup process
| * | `nas.hostname` | desired hostname of the NAS
| * | `nas.user.name` | name of the artificial NAS user that is used for service tasks (will be created if missing)
| * | `backup_location` | path to store all user data at. defaults to the primary mount, if specified
| * | `sshd.allow_additional_users` | list of users that are allowed to SSH into the NAS, in addition to the individual users
| * | `sshd.enable_password_authentication` | flag to enable the password-based authentication for SSH clients (default: `false`). strongly discouraged, specify authorized keys per user instead
| * | `users` | list of individual users, each having a user `name`, an `initial_password` hash, a `nextcloud_password` and a `samba_password` (if the respective features are used). you may also specify a `backup_folder_name` (defaults to username) and additional group assignments (`groups`) on the OS level. you may as well specify a list of `authorized_keys` for passwordless SSH access
| mount | `mounts_base_dir` | directory for mount points for external storage devices
| mount | `mounts.primary.*` | primary mount point that all user data will be stored at, by default. mounted via the `uuid` field. use `fstype` and `opts` to specify the filesystem and options of the mount (defaults fit NTFS-formatted drives)
| mount | `mounts.secondary.*` | optional secondary mount point, that the primary mount point will be mirrored to (same configuration options available)
| auto-upgrade, cloud | `mailing.*` | `server`, `user` name, `address`, `password` and `sender_name` for the mail account to be used for sending mails (e.g. notifications, password resets)
| auto-upgrade, cloud | `mail_recipient` | recipient for administrator mails (e.g. performed upgrades)
| cloud | `nas.domain` | public domain to access the machine
| cloud | `dynv6.token` | token to update the current IP address of the NAS domain on DynV6
| samba | `samba.internal_shares` | internal Samba shares, each having a `name` and a `path`, that are accessible with any account
| samba | `samba.public_shares` | public Samba shares, each having a `name` and a `path`, that are accessible even without an account
| dlna | `minidlna.*` | `display_name` to be shown in client devices, `directories` to list paths that should be accessible for clients
| mumble | `mumble.password` | password that is required by clients to connect (may be empty)
### Participants

### Vault
There are multiple participants in such an OpenVPN setup:

An Ansible vault is a place for storing credentials and sensitive information alike in an encrypted manner. Please refer to the [official documentation](https://docs.ansible.com/ansible/latest/user_guide/vault.html) if you want to learn how to create and use one.
1. certificate authority (CA): owns the key that will sign server and client certificates; may be an offline machine (more secure but requires manually copying files) or your Ansible host
2. server: accepts OpenVPN connections by clients; must be accessible via internet (best to use dynamic DNS)
3. clients: connect to the server; must have internet access to reach the server

Using a vault is mandatory if you want to have your credentials under version control, for example.
For simplicity, you may choose to not use a vault and assign your credentials to the variables of this playbook directly.
However, you should always strongly consider to use a vault for security reasons.
### Configuration

## Port Forwarding
Depending on a machine's role (i.e. type of participant), a different property has to be used in the respective Ansible inventory.

> **_NOTE:_** Instead of forwarding the relevant ports from your router to the NAS, you should strongly consider to use a VPN to avoid any unencrypted file transfer in the internet.
Use the `openvpn.server` property in the inventory of the machine that will act as the server and use the `openvpn.client` property for all remaining machines.

The NAS runs different services that operate on various ports.
When operating the NAS behind a router (i.e. firewall), these ports have to be accessible.
### Setup instructions

In case you insist on using port forwarding, the following ports are used by the respective services:
On the CA machine, initialize the CA, generating key and certificate:

Service | TCP | UDP
--------- |----------|---------
Samba | 139, 445 | 137, 138
OpenSSH | 22 |
Nextcloud | 80, 443 |
make openvpn-ca-init

These are the default ports. Strongly-consider to expose the services at non-default ports to prevent the load and risk that is caused by bots.
An execption are the ports 80 and 443, as they are required for the automatic certificate renewal.
On the Ansible host and for each participant, install OpenVPN, create a certificate request and retrieve it:

make ANSIBLE_INVENTORY=<inventory> openvpn-initialize

On the Ansible host, pack the retrieved requests:

make openvpn-pack-requests

>If you use a dedicated offline machine as CA:
>This will create a ZIP file including all retrieved requests at `./files-ca-transit/requests.zip`.
>Copy this archive file to the same relative path on the CA machine now.
On the CA machine, import and sign all requests:

make openvpn-ca-sign-requests

>If you use a dedicated offline machine as CA:
>This will create a ZIP file including all certificates at `./files-ca-transit/certificates.zip`.
>Copy this archive file to the same relative path on the Ansible host now.
Extract the certificate archive

make openvpn-extract-certificates

and distribute the certificates to each OpenVPN participant via

make ANSIBLE_INVENTORY=<inventory> openvpn-configure

This will also start the OpenVPN service on each machine and should leave you with a working OpenVPN setup (wait a few minutes).
26 changes: 26 additions & 0 deletions files/bendorf/id_nas_bendorf
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
$ANSIBLE_VAULT;1.1;AES256
39616637313631303837646632383036656463666438666165313738383238396166633535616337
6633393433633435343263623432333532373433323931610a313038383934363832353430646666
61353338623563666530333061386233396261386536326437646461383862623638393166346333
6130303130323661340a326363656433316638643264646238643863346130383862633362306134
32343231316339313035346366623137666434333939323138646335343430663731303732343537
62333864303966303935363036383434663062646330353164393766373637633135353862363737
34633262666538633634663064333331306239346234346365303931343832316461343533313230
65353135376561623264353738396639353033643134666166356430383234356535353130373434
35346265333438363362396132313566306337323738383966376464313865353737313536646634
31623537653336383638363932636131393733313034353533323038336363383235363663306666
31356537656235616266306632626331373736663661326131666165666366303265613836303464
38316264633732373837656365663138356230306633613362386233646436333131333130343636
65383539333863626635626439323261353664633135363133376436656433633666643936313436
30626565653537336631346465653030393933333466663166633734396331366466336463626533
36646466316161386435353163366433336330386362663638336534376430366138643734626565
38626531306365356461383562323738343534636533613238633230663336386464343763643466
62306239383735303631656161643931653634323235393436666330663162316362343135623736
39623236373866633734303761326365356630366362336638306362313536613165613261386636
64386537666637366532366430383239343334306531383437383635326533613466396463353338
65313237343361616530626231326165653835633037343036613333613338633331356537336437
31316364373233356337326638643136386430363831613138666136626231346664326565383164
38643938396664623866343866306434616333666631343333633136323439366261336263366365
30363331336531366132623662376531366432653232646661656563363762613934623064366238
39333236626238356134383236613663663961653631383866633032653364653761633035303137
64393930653839363837363437313837653038303130616637616166333536396635
1 change: 1 addition & 0 deletions files/bendorf/id_nas_koblenz.pub
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOzsiAPArvlwUlxyxkrHyUaggQgs8hgXr0EtuPfBvKKv sebschlicht@bastis-ideapad-5i
Loading

0 comments on commit b8dd7ef

Please sign in to comment.