---
weight: 4
title: "Instalación de VPS en Contabo"
date: 2024-10-31T13:56:35+0100
draft: false
summary: "Como montar tu propio VPS en Contabo"
categories:
- notes
tags:
- selfhosted
- vps
- debian
- fail2ban
- firewall
- zsh
- docker
---

{{< admonition type=info title="Este artículo ya es válido" open=true >}}

Este artículo no está completo, pero **ya es válido para iniciar un servidor VPS**.

Intentaremos completar (en algún momento del futuro) las secciones:
- Configuración simple de cortafuegos con **nftables**
- Configuración simple de cortafuegos con **firewalld**

En la siguiente entrega veremos como configurar **Traefik** como proxy inverso.

{{< /admonition >}}


Describimos la instalación de un VPS en el proveedor Contabo, pero quitando los detalles particulares de ese proveedor el resto de los pasos se pueden aplicar a cualquier VPS con **Debian 12 Server**



## Opciones en Contabo

Escogemos un servidor sencillo sin opciones extra.

|                   |                  |
|:------------------|:-----------------|
| Sistema Operativo | Debian 12 Server |
| Tipo de servidor  | Cloud VPS 1      |
| Cores             | 4 vCPU           |
| Memoria           | 6 Gb RAM         |
| HD                | 400 Gb           |

- En el proceso de instalación especificamos que queremos Debian 12
- Es importante recordar la password de ***root*** aunque ahora hay una opción para resetearla desde la web de contabo
- **ATENCIÓN: Evita usar caracteres especiales y las letras 'y' y 'z' al establecer la password de *root***. Hay un problema con los `LOCALES` y con el mapa de teclado que solucionaremos más tarde
- **Los snapshots de los servidores se borran a los 30 dias**
- En el panel de control web, sección *Your Services* debajo del botón *Manage* tenemos la opción para asignarle un nombre al servidor, yo lo hago con todos por que me es más sencillo recordarlos e interpretar las facturas
- Si hay problemas tendrás que recurrir al acceso via VNC. En la web de control de servidores en Contabo tienes la info para conectarte al VNC. Tienes que poner una contraseña para acceder al servidor VNC **que contenga mayúsculas, minúsculas, números y tenga EXACTAMENTE ocho caracteres**. Puedes dejar el acceso VNC desactivado y activarlo solo cuando lo necesitas.

## Instalación

### Actualización de paquetes

Con nuestro S.O. escogido (Debian 12) recién instalado en el VPS nos conectamos desde nuestro PC.

```bash
ssh -o PreferredAuthentications=password root@<serverIp>
```

{{< admonition type=tip title="¿Cómo que _PreferredAuthentications_?" open=false >}}

En mi estación de trabajo (desde donde me conecto a mis servidores) tengo configurado el acceso ssh via claves GPG. Si haces como yo y usas distintas claves por cada servidor remoto es posible se acumulen tantas claves que la conexión falle con el error *Too many authentication failures*. Esto ocurre por que `ssh` irá probando todas las claves disponibles para la conexión. Como el servidor aun no está configurado para aceptar claves fallarán todos los intentos de conexión, aparecerá el error y el servidor remoto rechazará la conexión. Para evitar eso usamos la opción:

`Preferredauthentications=password`

Otra forma de conectarse sería:

`ssh -o IdentitiesOnly=yes root@<vps_ip_address>`


{{< /admonition >}}


Y hacemos la típica actualización de todos los paquetes:

```bash
apt update
apt upgrade
```

Lo más seguro es que nos de un fallo de `locale settings` de este estilo

```log
perl: warning: Please check that your locale settings:
	LANGUAGE = "en_GB:en",
	LC_ALL = (unset),
	LC_TIME = "es_ES.UTF-8",
	LC_MONETARY = "es_ES.UTF-8",
	LC_ADDRESS = "es_ES.UTF-8",
	LC_TELEPHONE = "es_ES.UTF-8",
	LC_NAME = "es_ES.UTF-8",
	LC_MEASUREMENT = "es_ES.UTF-8",
	LC_IDENTIFICATION = "es_ES.UTF-8",
	LC_NUMERIC = "es_ES.UTF-8",
	LC_PAPER = "es_ES.UTF-8",
	LANG = "en_GB.UTF-8"
    are supported and installed on your system.
```
Lo arreglamos en la siguiente sección.

### Reconfigurar *locales* y mapa de teclado

Reconfiguramos los *locales*:

```bash
dpkg-reconfigure locales
```

Dejamos el `en_US.UTF-8` como *locale* por defecto.

Aparentemente Debian 12 ha pasado a controlar los locales bajo **systemd**. Parece que hay conflictos, por que el antiguo comando `locale` nos dice una cosa y el nuevo comando `localectl status` nos dice otra:

```bash
locale -a    # Lista de los locales que he generado con dpkg-reconfigure locales
C
C.utf8
en_GB.utf8
en_US.utf8
es_ES.utf8
eu_ES.utf8
gl_ES.utf8
POSIX

locale       # Antiguo comando para ver la configuración de locales
LANG=en_GB.UTF-8
LANGUAGE=en_GB:en
LC_CTYPE="en_GB.UTF-8"
LC_NUMERIC=es_ES.UTF-8
LC_TIME=es_ES.UTF-8
LC_COLLATE="en_GB.UTF-8"
LC_MONETARY=es_ES.UTF-8
LC_MESSAGES="en_GB.UTF-8"
LC_PAPER=es_ES.UTF-8
LC_NAME=es_ES.UTF-8
LC_ADDRESS=es_ES.UTF-8
LC_TELEPHONE=es_ES.UTF-8
LC_MEASUREMENT=es_ES.UTF-8
LC_IDENTIFICATION=es_ES.UTF-8
LC_ALL=

localectl status       # Nuevo comando para ver la configuración de locales
System Locale: LANG=en_US.UTF-8
               LANGUAGE=en_GB:en
    VC Keymap: (unset)
   X11 Layout: de
    X11 Model: pc105
```

En cualquier caso ya no nos aparecerán más errores de locales al hacer instalaciones de paquetes, pero tenemos que solucionar también el problema del mapa de teclado que nos interfiere al conectarnos via VNC desde la página web de administración de Contabo. Para ello ejecutamos `dpkg-reconfigure keyboard-configuration`, escogiendo las opciones (dependen del teclado que uses):

- **Keyboard model**: *Generic 105-key PC*
- **Country of origin**: *Spanish*
- **Keyboard Layout**: *Spanish*
- **Altgrn key**: *Default*
- **Compose key**: *No compose key*

Después de reconfigurar ya tendremos el mapa correcto :

```bash
localectl status
System Locale: LANG=en_US.UTF-8
               LANGUAGE=en_GB:en
    VC Keymap: (unset)
   X11 Layout: es
    X11 Model: pc105
```

Con esto no deberíamos tener ya problemas para hacer login con el VNC sea cual sea la contraseña de nuestro usuario `root`.

{{< admonition type=info title="Contraseñas" open=true >}}

Una vez que compruebes que el teclado funciona bien via VNC y que la configuración de *locales* ya no te da problema puedes cambiar las contraseñas y poner la que quieras. Pero en los siguientes pasos vamos a prohibir el acceso de `root` y vamos a exigir que el acceso por ssh se haga con claves GPG.
Eso si, por VNC desde la página de administración de Contabo, siempre podrás entrar como `root`.

{{< /admonition >}}



### Instalación de **git** y **etckeeper**

**git** lo uso continuamente, **etckeeper** se encarga de mantener todos los cambios que haya en el directorio `/etc/` de nuestro servidor controladas con **git**. A mi ya me ha resultado muy útil para resolver un par de problemas causados por cambios de configuración o instalación de paquetes. Uso el **etckeeper** estrictamente en local, nunca he definido un *remote* para este repo de **git**.

**git-crypt** y **gnupg** son una manera de almacenar información confidencial en un repo de git. Aunque **gnupg** vale para muchas más cosas y merece la pena tenerlo instalado.

Instalamos `git` y `etckeeper` (los siguientes comandos hay que ejecutarlos como `root` para tener el git configurado para **etckeeper**):

```bash
apt install git
apt install git-crypt
apt install gnupg
git config --global user.email "whatever@mail.com"  # Pon el correo que te convenga
git config --global user.name "Name Surname"        # Pon el nombre de usuario que tu quieras
apt install etckeeper
```

### Instalación de utilidades

Bibliotecas de compresión:

```bash
apt install zip unzip unace bzip2 lzop p7zip p7zip-full
```

Utilidades:

```bash
apt install most mc tree tmux aptitude rsync fasd ripgrep tldr
```

Instalamos `sudo`:

```bash
apt install sudo
```

Instalamos `ufw`

```bash
apt install ufw
```

Instalamos `zsh`:

```bash
apt install zsh
```

Instalamos `emacs`:

```bash
apt install emacs-nox
```


El comando completo de instalación sería:

```bash
apt install zip unzip unace bzip2 lzop p7zip p7zip-full \
most mc tree tmux aptitude rsync fasd \
sudo ufw zsh emacs-nox
```


## Creación de usuarios en el VPS

Vamos a añadir usuarios (lo de los id de los usuarios son costumbres mias que no tienen justificación clara):

```bash
adduser --uid=1111 hostadmin  # Siempre asigno el mismo id a los usuarios de administración
gpasswd -a hostadmin sudo

adduser salvari               # Por costumbre dejo un usuario personal con id 1000
gpasswd -a salvari sudo

adduser dockadmin             # Acostumbro a tener el dockadmin en el id 1001
gpasswd -a dockadmin sudo
```

## Conexión al VPS via **ssh**

### **ssh** en el cliente

En nuestra estación de trabajo preparamos las claves de acceso al server remoto y las trasferimos al mismo. Lo dos primeros comandos son de ejemplo, con una sola clave del tipo que sea te vale.

```bash
ssh-keygen -b 4096 -t rsa    # Especificamos ~/.ssh/<serverName>_rsa como salida

ssh-keygen -t ed25519 -a 100    # <-- Opción mas robusta (especificamos como salida ~/.ssh/<serverName>_ed25519)

ssh-copy-id -i .ssh/<serverName>_ed25519 -o PreferredAuthentications=password remoteuser@host
```

Para dejar correctamente configurado el acceso ssh al VPS desde nuestra estación de trabajo, tenemos que configurar nuestro cliente ssh editando el fichero `~/.ssh/config`. Hay también un fichero global con opciones para el cliente ssh que sería `/etc/ssh/ssh_config`. Los dos ficheros tienen el mismo formato.

```bash
touch ~/.ssh/config      # Creamos el fichero si no existe
chmod 600 ~/.ssh/config  # Solo nuestro usuario puede verlo, esto es imprescindible, ssh es muy susceptible
```

Editamos el fichero que tendría este contenido (evidentemente **tienes que usar tus propias direcciones ip y ficheros de claves**)

```config
# This options deactivates the use of every key in the ssh-agent
# Only the keys specified in this file will be used
# Host *
#     IdentitiesOnly=yes

Host firstvps
    HostName <firstvps_ip_address>
    User <user_name>
    Port 22
    IdentityFile ~/.ssh/firstvps_rsa

Host <secondvps_ip_address>
    HostName <secondvps_ip_address>
    User <user_name>
    Port 22
    IdentityFile ~/.ssh/secondvps_rsa
```

Con esta configuración y **una vez configurado el ssh en el servidor** (ver siguiente apartado) el comando de conexión será simplemente:

`ssh <serverName>`

Puedes conectarte a distintos usuarios en el lado VPS siempre y cuando hayas copiado la clave pública correspondiente al fichero `~/.ssh/authorized_keys` de ese usuario remoto.

### Configuración del servidor ssh en el VPS

En nuestro servidor VPS cambiamos la configuración para que:
- **No** se admitan login via ssh con password (**¡OJO! Esta es la opción que tendrás que habilitar de nuevo si te lias con tus claves gpg para ssh**)
- **No** se admita login via ssh con el usuario `root`

Tenemos que editar el fichero de configuración `/etc/ssh/sshd_config`. A continuación los cambios introducidos

```config
.
.
.
# 20240619-salvari uncomment
PubkeyAuthentication yes
.
.
.
# To disable tunneled clear text passwords, change to no here!
# 20240619-salvari changed to no
PasswordAuthentication no
#PermitEmptyPasswords no
.
.
.
# 20240619-salvari changed to no
UsePAM no
.
.
.
# 20240619-salvari changed to no
PermitRootLogin no
```

Después de cambiar la configuración reiniciamos el servicio con `sudo systemctl restart ssh`

Ahora tenemos que comprobar que podemos conectarnos desde nuestra estación de trabajo via clave gpg, por ejemplo con el comando que ya describimos: `ssh <serverName>`

## Configuración del cortafuegos

Ya hace tiempo que en Linux se ha pasado de implementar los _firewall_ con **iptables** a usar **nftables**. Para mantener la compatibilidad con el antiguo **iptables** hay utilidades que se encargan de traducir los comandos **iptables** a configuraciones con **nftables**

Yo solía usar *Uncomplicated Firewall* (UFW) para configurar mis cortafuegos por que mi configuración era muy simple. El problema es que **UFW** sigue usando comandos **iptables** y no se si la traducción a **nftables** queda muy elegante así que aunque de momento lo dejemos con **UFW** voy a echar un ojo a la configuración directa con **nftables** y también a usar **Firewalld** en lugar de **UFW**.

Las ventajas de usar directamente **nftables** son configuraciones más simples y que aprendes como usuar **nftables** y como funciona un cortafuegos.

La principal ventaja de usar **UFW** o **Firewalld** es que los escenarios y aplicaciones típicos son muy fáciles de configurar.

### UFW

Habíamos instalado **ufw** al instalar las utilidades al principio. Ahora vamos a poner la configuración básica del cortafuegos, es decir, vamos a permitir exclusivamente conexiones via ssh:

```bash
sudo ufw status verbose         # Comprobamos que está deshabilitado
sudo ufw app list               # comprobamos que apps soporta
sudo ufw default deny incoming  # le decimos que por defecto no acepta nada entrante
sudo ufw default allow outgoing # por defecto permitimos conexiones salientes
sudo ufw allow ssh              # permitimos conexiones ssh
sudo ufw enable                 # lo habilitamos
sudo ufw status verbose         # vemos el estado del cortafuegos
```

La respuesta al último comando tendrá esta pinta:

```bash
Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), disabled (routed)
New profiles: skip

To                         Action      From
--                         ------      ----
22/tcp                     ALLOW IN    Anywhere
22/tcp (v6)                ALLOW IN    Anywhere (v6)
```

**Según vayamos añadiendo servicios tendremos que ir añadiendo reglas al *firewall*.**

Si queremos ver como queda esto configurado en **nftables** podemos ejecutar el comando `sudo nft list ruleset`. Antes de activar el cortafuegos el comando no devuelve nada puesto que no hay definida ninguna regla. Pero tras activarlo obtenemos la configuración de reglas que figura a continuación. Son muchísimas reglas para lo que queremos (permitir sólo el acceso via **ssh**) Es de suponer que el **UFW** deja todo preparado para añadir nuevas reglas en el futuro. Aun así si configuramos lo mismo directamente con comandos **nftables** dudo mucho que necesitemos más de 30 lineas para dejar todo programado y bien comentado en el fichero de configuración.

```nftables
# Warning: table ip filter is managed by iptables-nft, do not touch!
table ip filter {
	chain ufw-before-logging-input {
	}

	chain ufw-before-logging-output {
	}

	chain ufw-before-logging-forward {
	}

	chain ufw-before-input {
		iifname "lo" counter packets 0 bytes 0 accept
		ct state related,established counter packets 35 bytes 3502 accept
		ct state invalid counter packets 0 bytes 0 jump ufw-logging-deny
		ct state invalid counter packets 0 bytes 0 drop
		meta l4proto icmp icmp type destination-unreachable counter packets 0 bytes 0 accept
		meta l4proto icmp icmp type time-exceeded counter packets 0 bytes 0 accept
		meta l4proto icmp icmp type parameter-problem counter packets 0 bytes 0 accept
		meta l4proto icmp icmp type echo-request counter packets 0 bytes 0 accept
		udp sport 67 udp dport 68 counter packets 54 bytes 18700 accept
		counter packets 4 bytes 252 jump ufw-not-local
		ip daddr 224.0.0.251 udp dport 5353 counter packets 0 bytes 0 accept
		ip daddr 239.255.255.250 udp dport 1900 counter packets 0 bytes 0 accept
		counter packets 3 bytes 152 jump ufw-user-input
	}

	chain ufw-before-output {
		oifname "lo" counter packets 0 bytes 0 accept
		ct state related,established counter packets 31 bytes 5156 accept
		counter packets 0 bytes 0 jump ufw-user-output
	}

	chain ufw-before-forward {
		ct state related,established counter packets 0 bytes 0 accept
		meta l4proto icmp icmp type destination-unreachable counter packets 0 bytes 0 accept
		meta l4proto icmp icmp type time-exceeded counter packets 0 bytes 0 accept
		meta l4proto icmp icmp type parameter-problem counter packets 0 bytes 0 accept
		meta l4proto icmp icmp type echo-request counter packets 0 bytes 0 accept
		counter packets 0 bytes 0 jump ufw-user-forward
	}

	chain ufw-after-input {
		udp dport 137 counter packets 0 bytes 0 jump ufw-skip-to-policy-input
		udp dport 138 counter packets 0 bytes 0 jump ufw-skip-to-policy-input
		tcp dport 139 counter packets 0 bytes 0 jump ufw-skip-to-policy-input
		tcp dport 445 counter packets 1 bytes 52 jump ufw-skip-to-policy-input
		udp dport 67 counter packets 0 bytes 0 jump ufw-skip-to-policy-input
		udp dport 68 counter packets 0 bytes 0 jump ufw-skip-to-policy-input
		fib daddr type broadcast counter packets 0 bytes 0 jump ufw-skip-to-policy-input
	}

	chain ufw-after-output {
	}

	chain ufw-after-forward {
	}

	chain ufw-after-logging-input {
		limit rate 3/minute burst 10 packets counter packets 1 bytes 40 log prefix "[UFW BLOCK] "
	}

	chain ufw-after-logging-output {
	}

	chain ufw-after-logging-forward {
		limit rate 3/minute burst 10 packets counter packets 0 bytes 0 log prefix "[UFW BLOCK] "
	}

	chain ufw-reject-input {
	}

	chain ufw-reject-output {
	}

	chain ufw-reject-forward {
	}

	chain ufw-track-input {
	}

	chain ufw-track-output {
		meta l4proto tcp ct state new counter packets 0 bytes 0 accept
		meta l4proto udp ct state new counter packets 0 bytes 0 accept
	}

	chain ufw-track-forward {
	}

	chain INPUT {
		type filter hook input priority filter; policy drop;
		counter packets 95 bytes 22558 jump ufw-before-logging-input
		counter packets 95 bytes 22558 jump ufw-before-input
		counter packets 5 bytes 296 jump ufw-after-input
		counter packets 4 bytes 244 jump ufw-after-logging-input
		counter packets 4 bytes 244 jump ufw-reject-input
		counter packets 4 bytes 244 jump ufw-track-input
	}

	chain OUTPUT {
		type filter hook output priority filter; policy accept;
		counter packets 31 bytes 5156 jump ufw-before-logging-output
		counter packets 31 bytes 5156 jump ufw-before-output
		counter packets 0 bytes 0 jump ufw-after-output
		counter packets 0 bytes 0 jump ufw-after-logging-output
		counter packets 0 bytes 0 jump ufw-reject-output
		counter packets 0 bytes 0 jump ufw-track-output
	}

	chain FORWARD {
		type filter hook forward priority filter; policy drop;
		counter packets 0 bytes 0 jump ufw-before-logging-forward
		counter packets 0 bytes 0 jump ufw-before-forward
		counter packets 0 bytes 0 jump ufw-after-forward
		counter packets 0 bytes 0 jump ufw-after-logging-forward
		counter packets 0 bytes 0 jump ufw-reject-forward
		counter packets 0 bytes 0 jump ufw-track-forward
	}

	chain ufw-logging-deny {
		ct state invalid limit rate 3/minute burst 10 packets counter packets 0 bytes 0 return
		limit rate 3/minute burst 10 packets counter packets 0 bytes 0 log prefix "[UFW BLOCK] "
	}

	chain ufw-logging-allow {
		limit rate 3/minute burst 10 packets counter packets 0 bytes 0 log prefix "[UFW ALLOW] "
	}

	chain ufw-skip-to-policy-input {
		counter packets 1 bytes 52 drop
	}

	chain ufw-skip-to-policy-output {
		counter packets 0 bytes 0 accept
	}

	chain ufw-skip-to-policy-forward {
		counter packets 0 bytes 0 drop
	}

	chain ufw-not-local {
		fib daddr type local counter packets 4 bytes 252 return
		fib daddr type multicast counter packets 0 bytes 0 return
		fib daddr type broadcast counter packets 0 bytes 0 return
		limit rate 3/minute burst 10 packets counter packets 0 bytes 0 jump ufw-logging-deny
		counter packets 0 bytes 0 drop
	}

	chain ufw-user-input {
		tcp dport 22 counter packets 1 bytes 60 accept
	}

	chain ufw-user-output {
	}

	chain ufw-user-forward {
	}

	chain ufw-user-logging-input {
	}

	chain ufw-user-logging-output {
	}

	chain ufw-user-logging-forward {
	}

	chain ufw-user-limit {
		limit rate 3/minute counter packets 0 bytes 0 log prefix "[UFW LIMIT BLOCK] "
		counter packets 0 bytes 0 reject
	}

	chain ufw-user-limit-accept {
		counter packets 0 bytes 0 accept
	}
}

# Warning: table ip6 filter is managed by iptables-nft, do not touch!
table ip6 filter {
	chain ufw6-before-logging-input {
	}

	chain ufw6-before-logging-output {
	}

	chain ufw6-before-logging-forward {
	}

	chain ufw6-before-input {
		iifname "lo" counter packets 0 bytes 0 accept
		rt type 0 counter packets 0 bytes 0 drop
		ct state related,established counter packets 0 bytes 0 accept
		meta l4proto ipv6-icmp icmpv6 type echo-reply counter packets 0 bytes 0 accept
		ct state invalid counter packets 0 bytes 0 jump ufw6-logging-deny
		ct state invalid counter packets 0 bytes 0 drop
		meta l4proto ipv6-icmp icmpv6 type destination-unreachable counter packets 0 bytes 0 accept
		meta l4proto ipv6-icmp icmpv6 type packet-too-big counter packets 0 bytes 0 accept
		meta l4proto ipv6-icmp icmpv6 type time-exceeded counter packets 0 bytes 0 accept
		meta l4proto ipv6-icmp icmpv6 type parameter-problem counter packets 0 bytes 0 accept
		meta l4proto ipv6-icmp icmpv6 type echo-request counter packets 0 bytes 0 accept
		meta l4proto ipv6-icmp icmpv6 type nd-router-solicit ip6 hoplimit 255 counter packets 0 bytes 0 accept
		meta l4proto ipv6-icmp icmpv6 type nd-router-advert ip6 hoplimit 255 counter packets 0 bytes 0 accept
		meta l4proto ipv6-icmp icmpv6 type nd-neighbor-solicit ip6 hoplimit 255 counter packets 0 bytes 0 accept
		meta l4proto ipv6-icmp icmpv6 type nd-neighbor-advert ip6 hoplimit 255 counter packets 0 bytes 0 accept
		meta l4proto ipv6-icmp xt match icmp6 ip6 hoplimit 255 counter packets 0 bytes 0 accept
		meta l4proto ipv6-icmp xt match icmp6 ip6 hoplimit 255 counter packets 0 bytes 0 accept
		meta l4proto ipv6-icmp ip6 saddr fe80::/10 xt match icmp6 counter packets 0 bytes 0 accept
		meta l4proto ipv6-icmp ip6 saddr fe80::/10 xt match icmp6 counter packets 0 bytes 0 accept
		meta l4proto ipv6-icmp ip6 saddr fe80::/10 xt match icmp6 counter packets 0 bytes 0 accept
		meta l4proto ipv6-icmp ip6 saddr fe80::/10 xt match icmp6 counter packets 0 bytes 0 accept
		meta l4proto ipv6-icmp xt match icmp6 ip6 hoplimit 255 counter packets 0 bytes 0 accept
		meta l4proto ipv6-icmp xt match icmp6 ip6 hoplimit 255 counter packets 0 bytes 0 accept
		meta l4proto ipv6-icmp ip6 saddr fe80::/10 xt match icmp6 ip6 hoplimit 1 counter packets 0 bytes 0 accept
		meta l4proto ipv6-icmp ip6 saddr fe80::/10 xt match icmp6 ip6 hoplimit 1 counter packets 0 bytes 0 accept
		meta l4proto ipv6-icmp ip6 saddr fe80::/10 xt match icmp6 ip6 hoplimit 1 counter packets 0 bytes 0 accept
		meta l4proto ipv6-icmp xt match icmp6 counter packets 0 bytes 0 accept
		meta l4proto ipv6-icmp xt match icmp6 counter packets 0 bytes 0 accept
		meta l4proto ipv6-icmp xt match icmp6 counter packets 0 bytes 0 accept
		meta l4proto ipv6-icmp xt match icmp6 counter packets 0 bytes 0 accept
		ip6 saddr fe80::/10 ip6 daddr fe80::/10 udp sport 547 udp dport 546 counter packets 0 bytes 0 accept
		ip6 daddr ff02::fb udp dport 5353 counter packets 0 bytes 0 accept
		ip6 daddr ff02::f udp dport 1900 counter packets 0 bytes 0 accept
		counter packets 0 bytes 0 jump ufw6-user-input
	}

	chain ufw6-before-output {
		oifname "lo" counter packets 0 bytes 0 accept
		rt type 0 counter packets 0 bytes 0 drop
		ct state related,established counter packets 0 bytes 0 accept
		meta l4proto ipv6-icmp icmpv6 type destination-unreachable counter packets 0 bytes 0 accept
		meta l4proto ipv6-icmp icmpv6 type packet-too-big counter packets 0 bytes 0 accept
		meta l4proto ipv6-icmp icmpv6 type time-exceeded counter packets 0 bytes 0 accept
		meta l4proto ipv6-icmp icmpv6 type parameter-problem counter packets 0 bytes 0 accept
		meta l4proto ipv6-icmp icmpv6 type echo-request counter packets 0 bytes 0 accept
		meta l4proto ipv6-icmp icmpv6 type echo-reply counter packets 0 bytes 0 accept
		meta l4proto ipv6-icmp icmpv6 type nd-router-solicit ip6 hoplimit 255 counter packets 0 bytes 0 accept
		meta l4proto ipv6-icmp icmpv6 type nd-neighbor-advert ip6 hoplimit 255 counter packets 0 bytes 0 accept
		meta l4proto ipv6-icmp icmpv6 type nd-neighbor-solicit ip6 hoplimit 255 counter packets 0 bytes 0 accept
		meta l4proto ipv6-icmp icmpv6 type nd-router-advert ip6 hoplimit 255 counter packets 0 bytes 0 accept
		meta l4proto ipv6-icmp xt match icmp6 ip6 hoplimit 255 counter packets 0 bytes 0 accept
		meta l4proto ipv6-icmp xt match icmp6 ip6 hoplimit 255 counter packets 0 bytes 0 accept
		meta l4proto ipv6-icmp ip6 saddr fe80::/10 xt match icmp6 counter packets 0 bytes 0 accept
		meta l4proto ipv6-icmp ip6 saddr fe80::/10 xt match icmp6 counter packets 0 bytes 0 accept
		meta l4proto ipv6-icmp ip6 saddr fe80::/10 xt match icmp6 counter packets 0 bytes 0 accept
		meta l4proto ipv6-icmp ip6 saddr fe80::/10 xt match icmp6 counter packets 0 bytes 0 accept
		meta l4proto ipv6-icmp xt match icmp6 ip6 hoplimit 255 counter packets 0 bytes 0 accept
		meta l4proto ipv6-icmp xt match icmp6 ip6 hoplimit 255 counter packets 0 bytes 0 accept
		meta l4proto ipv6-icmp ip6 saddr fe80::/10 xt match icmp6 ip6 hoplimit 1 counter packets 0 bytes 0 accept
		meta l4proto ipv6-icmp ip6 saddr fe80::/10 xt match icmp6 ip6 hoplimit 1 counter packets 0 bytes 0 accept
		meta l4proto ipv6-icmp ip6 saddr fe80::/10 xt match icmp6 ip6 hoplimit 1 counter packets 0 bytes 0 accept
		counter packets 0 bytes 0 jump ufw6-user-output
	}

	chain ufw6-before-forward {
		rt type 0 counter packets 0 bytes 0 drop
		ct state related,established counter packets 0 bytes 0 accept
		meta l4proto ipv6-icmp icmpv6 type destination-unreachable counter packets 0 bytes 0 accept
		meta l4proto ipv6-icmp icmpv6 type packet-too-big counter packets 0 bytes 0 accept
		meta l4proto ipv6-icmp icmpv6 type time-exceeded counter packets 0 bytes 0 accept
		meta l4proto ipv6-icmp icmpv6 type parameter-problem counter packets 0 bytes 0 accept
		meta l4proto ipv6-icmp icmpv6 type echo-request counter packets 0 bytes 0 accept
		meta l4proto ipv6-icmp icmpv6 type echo-reply counter packets 0 bytes 0 accept
		counter packets 0 bytes 0 jump ufw6-user-forward
	}

	chain ufw6-after-input {
		udp dport 137 counter packets 0 bytes 0 jump ufw6-skip-to-policy-input
		udp dport 138 counter packets 0 bytes 0 jump ufw6-skip-to-policy-input
		tcp dport 139 counter packets 0 bytes 0 jump ufw6-skip-to-policy-input
		tcp dport 445 counter packets 0 bytes 0 jump ufw6-skip-to-policy-input
		udp dport 546 counter packets 0 bytes 0 jump ufw6-skip-to-policy-input
		udp dport 547 counter packets 0 bytes 0 jump ufw6-skip-to-policy-input
	}

	chain ufw6-after-output {
	}

	chain ufw6-after-forward {
	}

	chain ufw6-after-logging-input {
		limit rate 3/minute burst 10 packets counter packets 0 bytes 0 log prefix "[UFW BLOCK] "
	}

	chain ufw6-after-logging-output {
	}

	chain ufw6-after-logging-forward {
		limit rate 3/minute burst 10 packets counter packets 0 bytes 0 log prefix "[UFW BLOCK] "
	}

	chain ufw6-reject-input {
	}

	chain ufw6-reject-output {
	}

	chain ufw6-reject-forward {
	}

	chain ufw6-track-input {
	}

	chain ufw6-track-output {
		meta l4proto tcp ct state new counter packets 0 bytes 0 accept
		meta l4proto udp ct state new counter packets 0 bytes 0 accept
	}

	chain ufw6-track-forward {
	}

	chain INPUT {
		type filter hook input priority filter; policy drop;
		counter packets 0 bytes 0 jump ufw6-before-logging-input
		counter packets 0 bytes 0 jump ufw6-before-input
		counter packets 0 bytes 0 jump ufw6-after-input
		counter packets 0 bytes 0 jump ufw6-after-logging-input
		counter packets 0 bytes 0 jump ufw6-reject-input
		counter packets 0 bytes 0 jump ufw6-track-input
	}

	chain OUTPUT {
		type filter hook output priority filter; policy accept;
		counter packets 0 bytes 0 jump ufw6-before-logging-output
		counter packets 0 bytes 0 jump ufw6-before-output
		counter packets 0 bytes 0 jump ufw6-after-output
		counter packets 0 bytes 0 jump ufw6-after-logging-output
		counter packets 0 bytes 0 jump ufw6-reject-output
		counter packets 0 bytes 0 jump ufw6-track-output
	}

	chain FORWARD {
		type filter hook forward priority filter; policy drop;
		counter packets 0 bytes 0 jump ufw6-before-logging-forward
		counter packets 0 bytes 0 jump ufw6-before-forward
		counter packets 0 bytes 0 jump ufw6-after-forward
		counter packets 0 bytes 0 jump ufw6-after-logging-forward
		counter packets 0 bytes 0 jump ufw6-reject-forward
		counter packets 0 bytes 0 jump ufw6-track-forward
	}

	chain ufw6-logging-deny {
		ct state invalid limit rate 3/minute burst 10 packets counter packets 0 bytes 0 return
		limit rate 3/minute burst 10 packets counter packets 0 bytes 0 log prefix "[UFW BLOCK] "
	}

	chain ufw6-logging-allow {
		limit rate 3/minute burst 10 packets counter packets 0 bytes 0 log prefix "[UFW ALLOW] "
	}

	chain ufw6-skip-to-policy-input {
		counter packets 0 bytes 0 drop
	}

	chain ufw6-skip-to-policy-output {
		counter packets 0 bytes 0 accept
	}

	chain ufw6-skip-to-policy-forward {
		counter packets 0 bytes 0 drop
	}

	chain ufw6-user-input {
		tcp dport 22 counter packets 0 bytes 0 accept
	}

	chain ufw6-user-output {
	}

	chain ufw6-user-forward {
	}

	chain ufw6-user-logging-input {
	}

	chain ufw6-user-logging-output {
	}

	chain ufw6-user-logging-forward {
	}

	chain ufw6-user-limit {
		limit rate 3/minute counter packets 0 bytes 0 log prefix "[UFW LIMIT BLOCK] "
		counter packets 0 bytes 0 reject
	}

	chain ufw6-user-limit-accept {
		counter packets 0 bytes 0 accept
	}
}
```

Si queremos ver el cortafuegos en acción podemos ejecutar el comando `sudo journalctl -S today |grep UFW`, la cantidad de intentos de acceso bloqueados nunca deja de asombrarme.


## Instalación y configuración de **fail2ban**

### Instalación de **fail2ban**

Primero necesitamos instalar **fail2ban** en nuestro VPS. Podemos usar simplemente:

```bash
sudo apt install fail2ban
```

Si por lo que sea queremos instalar la última versión, podemos hacerlo descargándola desde [aquí](https://github.com/fail2ban/fail2ban/releases) con los siguientes comandos:

```bash
# Echa un ojo al repo de releases y apunta la versión actual de fail2ban
# En nuestro caso es: https://github.com/fail2ban/fail2ban/releases/download/1.1.0/fail2ban_1.1.0-1.upstream1_all.deb

cd /tmp/

# 1) download deb package and signature:
wget https://github.com/fail2ban/fail2ban/releases/download/1.1.0/fail2ban_1.1.0-1.upstream1_all.deb \
     https://github.com/fail2ban/fail2ban/releases/download/1.1.0/fail2ban_1.1.0-1.upstream1_all.deb.asc

# 2) check signature (if you want to be sure file is unmodified):
gpg --verify fail2ban_1.1.0-1.upstream1_all.deb.asc fail2ban_1.1.0-1.upstream1_all.deb

# 3) view details of the package:
dpkg -I fail2ban_1.1.0-1.upstream1_all.deb

# 4) to ensure the upgrade run gentler (protocol of previous version may be incompatible), stop fail2ban before install:
# using service:
sudo service fail2ban stop
# using client:
sudo fail2ban-client stop

# 5a) either install package using dpkg (standalone package, don't regard dependencies):
sudo dpkg -i fail2ban_1.1.0-1.upstream1_all.deb
# if the package introduces some "broken" dependencies (I don't think so in case of fail2ban which has few dependencies),
# to fix the unmet dependency issue, run this:
sudo apt -f install

# 5b) alternatively install package using gdebi (that will take care of installation of dependencies):
sudo gdebi fail2ban_1.1.0-1.upstream1_all.deb
# if you want to check anyway whether there are some broken packages and fix them automatically, you can run:
sudo apt -f install
```

Una vez instalado **fail2ban** podemos comprobar el estado del servicio:

```bash
systemctl list-units -a --state=active --type=service
systemctl status fail2ban.service
```

### Configuración de **fail2ban**

Hay dos ficheros principales de configuración en **fail2ban**: `/etc/fail2ban/fail2ban.conf` y `/etc/fail2ban/jail.conf` (ver [aquí](https://github.com/fail2ban/fail2ban/wiki/Proper-fail2ban-configuration))

/etc/fail2ban/fail2ban.conf
: Es el fichero de configuración de las opciones de operación del demonio **fail2ban**. Aquí se especifican cosas como el *loglevel*, el fichero de log, el socket o el fichero del pid.

/etc/fail2ban/jail.conf
: Este es el fichero interesante, aquí es donde configuramos las reglas de baneado. Así aquí se configura, por ejemplo, el intervalo de baneo, el número de intentos de login antes de banear una ip, la lista blanca de IPs, la info que se envia por correo cuando se detecta un ataque, etc. etc.
: En **fail2ban** definimos un `jail` por cada servicio que nos interesa proteger.

En casi todos los sitios que he leido nos aconsejan es hacer copias de los dos ficheros con extensión `.local` y hacer la configuración en estas copias.

```bash
cd /etc/fail2ban/
cp fail2ban.conf fail2ban.local
cp jail.conf jail.local
```

Pero la verdad es que en el man de fail2ban nos dicen que en los ficheros `.local` pongamos **exclusivamente** lo que no coincida con los valores por defecto. Y así lo voy a configurar.

{{< admonition type=danger title="Cambio de ficheros de logs en Debian 12" open=true >}}

Debian 12 ha cambiado la forma de gestionar los logs del sistema, ahora se usa **journald** bajo el paragüas de **systemd**. Si echais un ojo al directorio  `/var/log` comprobareis que ya no existen la mayoria de los ficheros de log tradicionales.

Esto impacta directamente en la configuración de **fail2ban**. Hay que tenerlo en cuenta por que con las configuraciones típicas de ejemplo en internet (o mi antigua configuración) **ya no funciona**.

{{< /admonition >}}

Otro detalle de la configuración de **fail2ban** es que la mayoría de los linux actuales han cambiado del antiguo **iptables** a **nftables** para implementar las políticas de cortafuegos. Este cambio no es reciente pero yo no era muy consciente por que hay programas de "traducción" de reglas **iptables** a reglas **nftables** . Si investigas un poco en tu linux seguramente verás que cuando invocas `iptables` casi seguro que en realidad estás invocando a `xtables-nft-multi`.

Este no es un problema que impida el funcionamiento de **fail2ban**. Por ejemplo, ya comentamos que el cortafuegos que hemos instalado en nuestro server (*Uncomplicated Firewall* o **ufw**) sigue usando **iptables** y lo mismo hacen muchas otras aplicaciones como **Docker** sin ir más lejos. Asi que si no cambiamos la configuración por defecto **fail2ban** también usaría comandos de **iptables** que serían traducidos por **xtables-nft-multi**. Pero creo que lo conveniente es usar ya **nftables** puesto que  **fail2ban** lo soporta. [^1]

[^1]: Queda pediente buscar una alternativa a **ufw** que use directamente **nftables**, como **firewalld** por ejemplo.

Si desplegamos **fail2ban** por primera vez hay que ir con cuidado de que no nos bloquee a nosotros mismos fuera del sistema. Puede que quieras activar el acceso con password via ssh antes de hacer el despliegue de **fail2ban** y seguramente es mejor no desplegar políticas de *baneo* muy agresivas hasta comprobar que todo funciona bien.

Si antes de activar **fail2ban** quereis ver la cantidad de intentos de acceso ilegales[^2] que tiene vuestro VPS podéis ejecutar:

[^2]: Si también quereis ver los intentos de acceso que esta bloqueando **UFW** podeis ejecutar `sudo journalctl -S today |grep UFW`

```bash
journalctl -u ssh.service -S today
journalctl -u ssh -S "1 hour ago"q   # Limitado a la última hora, el parámetro u (unit) acepta regexp
```

Con todas estas consideraciones nuestro fichero `jail.local` inicial quedaría:

```toml
[DEFAULT]
# Add my others VPS as trusted servers
ignoreip = xxx.xxx.xxx.xxx yyy.yyy.yyy.yyy

banaction=nftables[type=multiport]
# banaction=nftables[type=allports] # optional, more aggresive


#
# JAILS
#

[sshd]
enabled = true
backend = systemd     # backend is systemd in Debian 12
```

En este fichero de configuración:
- Añado mis otros VPS a la lista `ignoreip` para que **fail2ban** nunca los banee. (Evidentemente para acceder tendre que configurar correctamente las claves ssh en esos otros VPS)
- Cambio el `banaction` para que **fail2ban** use **nftables** en lugar de **iptables**
- Activo un *jail* para el servicio `sshd` y, muy importante, especifico que el `backend` es `systemd`

Una vez configurado reiniciamos el servicio **fail2ban* con:

```bash
sudo systemctl restart fail2ban
```
Con esta parametrización estas son las ip baneadas a los pocos minutos de arrancar el servicio.

```bash
fail2ban-client status sshd
Status for the jail: sshd
|- Filter
|  |- Currently failed:	18
|  |- Total failed:	159
|  `- Journal matches:	_SYSTEMD_UNIT=sshd.service + _COMM=sshd
`- Actions
   |- Currently banned:	18
   |- Total banned:	18
   `- Banned IP list:	47.236.2.29 8.219.246.145 8.222.245.20 47.237.106.242 8.222.169.164 8.219.241.126 8.222.184.144 8.222.150.15 171.22.31.29 202.157.184.46 103.146.52.138 103.142.87.225 47.245.92.217 35.192.179.181 27.71.237.15 47.237.4.213 160.20.186.237 59.34.217.89
root@vmi1540613:/etc/fail2ban#
```

También podemos comprobar la regla añadida por **fail2ban** a la configuración de nuestro _firewall_ [^3]:

[^3]: Las ip de la regla no coinciden con las del status de **fail2ban** por que las he impreso en momentos muy separados en el tiempo.

```bash
sudo nft list table inet f2b-table

table inet f2b-table {
	set addr-set-sshd {
		type ipv4_addr
		elements = { 8.154.32.31, 47.245.124.212,
			     58.211.191.14, 125.124.215.61,
			     167.71.137.127, 194.169.175.37,
			     194.169.175.38, 208.84.154.106,
			     209.38.21.252, 211.224.41.185,
			     218.92.0.243 }
	}

	chain f2b-chain {
		type filter hook input priority filter - 1; policy accept;
		tcp dport 22 ip saddr @addr-set-sshd reject with icmp port-unreachable
	}
}
```




### Algunos comandos útiles:

- `fail2ban-client status` Comprobar estado
- `fail2ban-client --test` Comprueba la configuración para ver si hay errores
- `fail2ban-client reload` Recargar configuración
- `fail2ban-client status sshd` Comprobar estado del jail sshd (o cualquier otro) y ver las IP baneadas
- `fail2ban-client set <jail_name> unbanip <ip_address>` Desbanear una IP
- `fail2ban-client set <JAIL_NAME> addignoreip <IP_Address>` Meter una IP en la lista blanca (hasta el siguiente arranque)
- `fail2ban-client set <JAIL_NAME> delignoreip <IP_Address>` Quitar una IP de la lista blanca
- `fail2ban-client get <JAIL_NAME> ignoreip` Comprobar lista blanca para un jail

### Refinando la configuración

Cambiamos los tiempos de detección, de baneo inicial y activamos el `bantime.increment`.

```toml
[DEFAULT]

# Add my others VPS as trusted servers
ignoreip = xxx.xxx.xxx.xxx yyy.yyy.yyy.yyy

maxretry = 2
findtime = 60m

bantime.increment = true
bantime = 60m

banaction = nftables[type=multiport]
#banaction_allports = nftables[type=allports]

#
# JAILS
#

[sshd]
enabled = true
# To use more aggressive sshd modes set filter parameter "mode" in jail.local:
# normal (default), ddos, extra or aggressive (combines all).
# See "tests/files/logs/sshd" or "filter.d/sshd.conf" for usage example and details.
#mode   = normal
port    = ssh
logpath = %(sshd_log)s
# backend = %(sshd_backend)s
backend = systemd
```

{{< admonition type=tip title="Otras jail" open=true >}}

Evidentemente a medida que vayamos instalando servicios en nuestro VPS deberemos añadir las jail necesarias para que **fail2ban** monitorice intentos de acceso ilegal a esos servicios.

{{< /admonition >}}



## Instalación de zsh

Me gusta usar zsh para trabajar en la terminal así que lo vamos a instalar en nuestro VPS.

Antes de empezar vamos a instalar:

- **python3-all-dev** aunque sólo lo necesitas si vas a usar **pyenv** para gestionar entornos virtuales de Python3
- **antigen** para gestionar todos los plugins de **zsh**
- **zsh-git-prompt** para tener información de git en el prompt del sistema

Como **root** ejecutamos:

```bash
apt install python3-all-dev

cd /opt
git clone https://github.com/zsh-users/antigen
git clone https://github.com/olivierverdier/zsh-git-prompt
```

En el home de cada usuario (cada usario con el que queramos usar **zsh**) tendremos que crear un fichero `~/.zshrc` con las opciones de **zsh** que queramos para ese usuario y un fichero `~/.zalias.zsh` con los alias que queramos definir.

{{< admonition type=warning title="Error con el bundle docker" open=true >}}

He tenido que crear a mano el directorio:

`~/.antigen/bundles/robbyrussell/oh-my-zsh/cache/completions`

para solucionar un error con el bundle **docker** de **oh-my-zsh**
{{< /admonition >}}

**Contenido de ~/.zshrc**

```.zshrc
# This line loads .profile, it's experimental
[[ -e ~/.profile ]] && emulate sh -c 'source ~/.profile'
source /opt/zsh-git-prompt/zshrc.sh
source /opt/antigen/antigen.zsh

# Load the oh-my-zsh's library.
antigen use oh-my-zsh

antigen bundles <<EOFBUNDLES
  # Bundles from the default repo (robbyrussell's oh-my-zsh)
  git
  command-not-found
  tmux
  # extracts every kind of compressed file
  extract
  # jump to dir used frequently, switched from z to fasd
  # needs =apt install fasd=
  z
  fasd
  # common
  common-aliases
  # virtualenvwrapper (not compatible with pyenv)
  # robbyrussell/oh-my-zsh plugins/virtualenvwrapper
  # pyenv
  # mattberther/zsh-pyenv
  # commandline autocompletions
  zsh-users/zsh-completions
  # cmdline completion for docker
  docker
  # cmdline completion for docker compose
  docker-compose
  # cmdline completion for docker
  # docker-machine
  # Learn alias
  # MichaelAquilina/zsh-you-should-use
  alias-finder # alias-finder
  # zsh-users/zsh-history-substring-search
  # autosuggestions
  tarruda/zsh-autosuggestions
  # Syntax highlighting
  zsh-users/zsh-syntax-highlighting
EOFBUNDLES

#antigen theme agnoster
antigen theme gnzh

# Tell antigen that you're done.
antigen apply

# Correct rm alias from common-alias bundle
unalias rm
alias rmi='rm -i'

# Read alias from file
source ~/.zalias.zsh
```

**Contenido de ~/.zalias.zsh**

```zsh
#----------------------------------------------------------------------
# Some utilities

# list alias
alias lal='alias |grep '

# make directory and cd to it
amd() {
    mkdir -p $1
    cd $1
}

# systemd aliases
# Base
alias sc=systemctl
alias scu='systemctl --user'

# systemd subcommands
alias scst='sudo systemctl start'
alias scsp='sudo systemctl stop'
alias scrl='sudo systemctl reload'
alias scrt='sudo systemctl restart'
alias sce='sudo systemctl enable'
alias scd='sudo systemctl disable'

alias scs='systemctl status'
alias scsw='systemctl show'
alias sclu='systemctl list-units'
alias scluf='systemctl list-unit-files'
alias sclt='systemctl list-timers'
alias scc='systemctl cat'
alias scie='systemctl is-enabled'

# emacs
alias e='emacs'

# Public IP
alias myip='wget http://ipecho.net/plain -O - -q ; echo'
```

Puedes hacer el cambio de shell permanente para un usuario con el comando `chsh --shell /usr/bin/zsh`.


## Instalación de Docker

Todos (o casi todos) los servicios que vamos a instalar en el VPS van a estar *dockerizados* así que vamos a instalar **Docker**.

```bash
# Add Docker's official GPG key:
sudo apt-get update
sudo apt-get install ca-certificates curl
sudo curl -fsSL https://download.docker.com/linux/debian/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/debian \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update

apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

gpasswd -a dockadmin docker
```

Con esto tendríamos el **Docker** instalado en nuestro VPS. Para comprobarlo deberíamos ejecutar con nuestro usuario `dockadmin` el contenedor de pruebas con:

`docker run hello-world`


## Referencias

- [Introdución ao uso de nftables para construír devasas personalizadas](https://veedeo.org/w/c5R4mo2eEzx7s6aiwk7yBY)
- [nftables multi network (home) router primer](https://www.monotux.tech/posts/2021/04/nftables-primer/)
- [Firewall Configuration with nftables](https://travishorn.com/firewall-configuration-with-nftables)
- [Installing and Configuring fail2ban](https://www.naturalborncoder.com/2024/10/installing-and-configuring-fail2ban/) in Natural Born Coder blog.
- [Using Fail2ban to secure your server](https://www.linode.com/docs/guides/using-fail2ban-to-secure-your-server-a-tutorial/)
- [Fail2ban Configuration](https://www.hostinger.com/tutorials/fail2ban-configuration)
