Set up a private ClamAV database mirror with a Tor-based local SOCKS5 proxy
Tags: security, linux, tutorial, opensource
Published: 2023-02-09 11:11:31

# WARNING! NONE OF THE PERFORMED CONFIGURATIONS CONSIDER SPECIFIC PROJECTS' SECURITY REQUIREMENTS. USE THIS ARTICLE AS A SECONDARY SOURCE OF KNOWLEDGE ONLY AND HARDEN YOUR SYSTEMS AS APPROPRIATE.

## Contents
* [Preface](#preface)
* [Why set up a private mirror?](#why-set-up-a-private-mirror)
* [Setting up a server](#setting-up-a-server)
    * [What tools are used](#what-tools-are-used)
    * [Creating an LXC container on Proxmox](#creating-an-lxc-container-on-proxmox)
    * [A local Tor proxy (SOCKS5) with obfs4 bridges](#a-local-tor-proxy-socks5-with-obfs4-bridges)
    * [ClamAV with automatic database updates](#clamav-with-automatic-database-updates)
    * [Web server with HTTPS](#web-server-with-https)
    * [Enabling firewall](#enabling-firewall)
* [Configuring clients](#configuring-clients)
* [The result](#the-result)

## Preface
If you are an active computer user, you probably often get a lot of new files from the outside world. The environment is harsh, and it's quite easy to infect a device with malware. To reduce the chances of being infected, it's a good practice to scan everything with special software. For small files that don't contain any confidential info, services like [VirusTotal](https://virustotal.com) can be utilized. For other data, a standalone or a Live CD-based antivirus application should be preferred.

One of the most popular Free and Open Source applications of this kind is [ClamAV](https://clamav.net) by Cisco Talos. The tool is the de facto standard antivirus for UNIX-like systems (both on servers and clients).

To be able to find threats, antivirus software needs a special database of virus signatures. These signatures are continuously created by large teams of security specialists.

## Why set up a private mirror?
Well, there might be lots of reasons. The most significant one coming straight to mind is to reduce network traffic and to increase the database update speed for clients.

The decision to host a private virus signature database mirror for ClamAV at my Homelab emerged because I use the tool a lot, and it's not available in my region.

I encourage you to try out this small project even for your Homelab, because it is really easy!

## Setting up a server
### What tools are used
Since the aforementioned Homelab is based on the Proxmox VE hypervisor, everything is set up there, in a container.

> In this scenario, `freshclam` is used for fetching database files to a mirror server. It's not possible to serve CDIFF files using this method. Consider [cvdupdate](https://docs.clamav.net/appendix/CvdPrivateMirror.html#use-cvdupdate-to-serve-whole-databases-and-database-patch-files-from-a-private-mirror) if you need to reduce network bandwidth consumption even more during clients' database updates.

| What | Tool |
|------|------|
| Hypervisor | Proxmox VE 7.3-4 |
| Guest | Fedora Linux 37 (Container Image) |
| Antivirus (mirror) | ClamAV 0.103.7 |
| Anonymous communication | Tor 0.4.7.13 |

### Creating an LXC container on Proxmox
1. Select your storage pool for VM disks and container templates, go to `CT Templates` and click on the `Templates` button.
    
    ![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/17g1f5y3k9rnny16ma05.png)

1. Find the latest Fedora version in the list and download it.

    ![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7xjettkno5f8lg49xfjj.png)

1. Click on the `Create CT` button at the top of the Proxmox dashboard and follow the wizard. It's ok to designate 1 CPU core to the container, but it needs sufficient RAM to update databases. Set the limit to something around 4096 MiB.

1. Don't forget to set an option to start the container at boot.

    ![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a01mribgckzpimbwqiux.png)



1. Start up the container and connect to its console.

### A local Tor proxy (SOCKS5) with obfs4 bridges
In countries where Tor is censored, bridges can be used.

> Some operating systems' repositories don't provide pre-built packages for obfs4, but the tool can be easily [compiled](https://github.com/Yawning/obfs4) from source code. If you are interested in a quick guide on how to compile obfs4, let me know!

#### Install Tor
Thankfully, Fedora has everything we need in its repositories <3.

```
# dnf install tor obfs4
```

#### Configure Tor
1. Locate the `obfs4proxy` binary
    ```
    $ which obfs4proxy
    ```

    {% collapsible Example %}
    ```
    [deathroll@ClamAV-DB ~]$ which obfs4proxy
    /usr/bin/obfs4proxy
    [deathroll@ClamAV-DB ~]$
    ```
    {% endcollapsible %}

1. Open the `/etc/tor/torrc` config file with an editor of choice. Mine is vim.
    ```
    # vim /etc/tor/torrc
    ```

1. Next, add the following two lines:
    ```
    ClientTransportPlugin obfs4 exec /usr/bin/obfs4proxy
    UseBridges 1
    ```

    👆 Replace the path after `exec` with what you got in step 1.

1. Now, get some bridges!
Go to https://bridges.torproject.org/options/, select `obfs4`, and click on `Get Bridges`.

    ![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dv0uqq924h5d9mhc2889.png)

    In order to get a list of bridges, complete a captcha. When the list is displayed, just copy it as-is.

    
    ![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rshm8yrydq03bkqw9yv7.png)

1. Paste what you've just copied to the `torrc` file and prefix each line with `Bridge`. Here's how it will look in the file (the output was modified due to privacy concerns):
    ```
    [deathroll@ClamAV-DB ~]$ grep -i ^bridge /etc/tor/torrc 
    Bridge obfs4 <some.ip.address.here:port> <ALONGSTRINGOFSOMEJIBBERISH> cert=<ONEMORELONGSTRINGOFSOMEJIBBERISH> iat-mode=<number>
    Bridge obfs4 <some.ip.address.here:port> <ALONGSTRINGOFSOMEJIBBERISH> cert=<ONEMORELONGSTRINGOFSOMEJIBBERISH> iat-mode=<number>
    [deathroll@ClamAV-DB ~]$
    ```

1. After saving and closing the config file, perform a test connection by executing `tor` as `toranon`.
    ```
    $ sudo -u toranon tor
    ```

1. In case of success, kill the process with `Ctrl + C`, and enable and start the service.
    ```
    # systemctl enable --now tor
    ```

1. Check the status with the following command:
    ```
    # systemctl status tor
    ```

### ClamAV with automatic database updates
#### Install ClamAV
```
# dnf install clamav clamd clamav-update
```

#### Configure freshclam
1. Open the `/etc/freshclam.conf` file and add the following:
    ```
    HTTPProxyServer socks5://127.0.0.1
    HTTPProxyPort 9050
    ```

1. Enable database compression (optional).
    ```
    CompressLocalDatabase yes
    ```

1. Also, you can enable logging (optional). For more, read [freshclam.conf(5)](https://manpages.debian.org/bullseye/clamav-freshclam/freshclam.conf.5.en.html).
    
    1. Add this to the config:
        ```
        UpdateLogFile /var/log/freshclam.log
        LogTime yes
        LogSyslog yes
        ```

    1. Create the log file.
        ```
        # touch /var/log/freshclam.log
        ```

    1. And change its owner to `clamupdate`.
        ```
        # chown clamupdate: /var/log/freshclam.log
        ```

1. After that, locate the `clamav-freshclam` service file.
    ```
    $ find /etc/systemd/ -name '*clam*'
    ```

    {% collapsible Example %}
    ```
    [root@ClamAV-DB deathroll]# find /etc/systemd/ -name '*clam*'
    /etc/systemd/system/multi-user.target.wants/clamav-freshclam.service
    [root@ClamAV-DB deathroll]#
    ```
    {% endcollapsible %}

1. Open the file you've just found and append `tor.service` to the lines that begin with `Wants` and `After` so they look like this:
    ```
    Wants=network-online.target tor.service
    After=network-online.target tor.service
    ```

1. Reload the systemd manager configuration.
    ```
    # systemctl daemon-reload
    ```

1. Enable and start the `clamav-freshclam` service.
    ```
    # systemctl enable --now clamav-freshclam
    ```

1. Check the status with the following command:
    ```
    # systemctl status clamav-freshclam
    ```

### Web server with HTTPS
> In this example, we use NGINX, but you're free to experiment.

#### Prepare a directory for serving
1. Create a new directory. For me it is `/srv/cvd`.
    ```
    # mkdir -p /srv/cvd
    ```

1. Find where the database is located.
    ```
    # find / -name clamav -xtype d |& grep -iv permission
    ```

    👆 `-xtype d` makes `find` search for directories only. `|&` pipes both `stdout` and `stderr` to grep. Here we are just filtering the output to remove read errors—don't worry, they are insignificant.

    In my case, the dir is `/var/lib/clamav`.

    {% collapsible Example %}
    ```
    [deathroll@ClamAV-DB nginx]$ sudo find / -name clamav -xtype d |& grep -iv permission
    /usr/share/licenses/clamav
    /var/lib/clamav
    [deathroll@ClamAV-DB nginx]$
    ```
    {% endcollapsible %}

    {% collapsible Without grep filtering %}
    ```
    [deathroll@ClamAV-DB nginx]$ sudo find / -name clamav -xtype d
    find: ‘/proc/tty/driver’: Permission denied
    /usr/share/licenses/clamav
    /var/lib/clamav
    find: ‘/dev/.lxc/sys/kernel’: Permission denied
    find: ‘/dev/.lxc/sys/power’: Permission denied
    find: ‘/dev/.lxc/sys/class’: Permission denied
    find: ‘/dev/.lxc/sys/devices’: Permission denied
    find: ‘/dev/.lxc/sys/dev’: Permission denied
    ---OUTPUT TRIMMED---
    ```
    {% endcollapsible %}

1. Edit `/etc/fstab` to set a read-only bind mount from the database dir to the newly created one.
    ```
    /var/lib/clamav/    /srv/cvd    none    bind,ro    0   0
    ```

1. Reload the systemd manager configuration.
    ```
    # systemctl daemon-reload
    ```

1. Try to mount `/srv/cvd`.
    ```
    # mount /srv/cvd
    ```

1. Check the mountpoint.
    ```
    $ findmnt /srv/cvd
    ```

    {% collapsible Example %}
    ```
    [deathroll@ClamAV-DB nginx]$ findmnt /srv/cvd
    TARGET   SOURCE                                                 FSTYPE OPTIONS
    /srv/cvd NAS/Virtualization/subvol-1000-disk-0[/var/lib/clamav] zfs    ro,noatime,xattr,posixacl
    [deathroll@ClamAV-DB nginx]$
    ```
    {% endcollapsible %}

#### Install NGINX
```
# dnf install nginx
```

#### Copy certificates
> In order to be able to use HTTPS, it's required to have valid certificates. Refer to [this article](https://dev.to/deathroll/trusted-self-signed-tls-certificates-for-dummies-w-thorough-explanations-included-da7) to learn how to create a trusted self-signed certificate.

1. Create a directory for certificates in a preferred location.
    ```
    # mkdir /etc/ssl/www
    ```

1. Copy a chain of trust file and a key file to the newly created directory.

#### Configure NGINX
> In this example, it's assumed that a DNS A record exists for the container's static IP address. The record name is `cvd.deathroll.internal`. It's also possible to implement HTTPS without DNS, though not recommended.

> The configuration is split up into multiple files for ease of administration, but this is not required.

1. Create a file with shared SSL/TLS configuration for server blocks. Choose a filename suitable for you.
    ```
    # vim /etc/nginx/ssl_params.conf
    ```

    1. Add at least two things: locations for the chain of trust and key files.
        ```nginx
        ssl_certificate     "/etc/ssl/www/fullchain.pem";
        ssl_certificate_key "/etc/ssl/www/deathroll-internal-key.pem";
        ```

    1. Change any other [options](https://nginx.org/en/docs/http/ngx_http_ssl_module.html) at your discretion. For example:
        ```nginx
        ssl_prefer_server_ciphers on;
        ssl_protocols TLSv1.2 TLSv1.3;
        ```

1. Change the default server block in the `/etc/nginx/nginx.conf` file to the following:
    ```nginx
    server {
        listen      80;
        server_name _; # A catch-all server name for handling requests to any host.

        location / {
            return 301 https://$host$request_uri; # Redirect to HTTPS.
        }
    }
    ```

1. Create a file for a \<domain\> HTTPS server block in `/etc/nginx/conf.d`.
    ```
    vim /etc/nginx/conf.d/cvd.deathroll.internal.conf
    ```

    And populate it with contents similar to these:

    ```nginx
    server {
        listen      443 ssl;
        server_name cvd.deathroll.internal;

        root        /srv/cvd; # This is the bind mount directory you have created earlier.

        include     /etc/nginx/ssl_params.conf; # Include shared TLS/SSL parameters.

        location / {
            autoindex on; # Enable directory listing.
        }
   }
   ```

1. Create a file with a server block for denying requests to any host. For example, `/etc/nginx/conf.d/deny-conn-by-ip.conf`. Add something like this:
    ```nginx
    server {
        listen      443 default_server; # Make this server default for port 443 to be able to catch unwanted requests.
        server_name _;

        include     /etc/nginx/ssl_params.conf;

        location / {
            return 403;
        }
    }
    ```

    > 👆 From what I could infer from inspecting packets, observing NGINX behavior, and reading its [documentation](https://nginx.org/en/docs/http/request_processing.html), when NGINX receives a request, it checks the `Host` HTTP header to decide what virtual host should process the request.

    > ![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9wc6bzidccy19ys98271.png)

    > The catch-all (`_`) server for port 443 has a `default_server` parameter, so HTTPS requests to any `Host` will be processed by this virtual host. If this parameter is omitted, any other virtual server for this port may become the default server, and hence requests won't be denied.

1. Reload NGINX config.
    ```
    # systemctl reload nginx
    ```

1. Check NGINX status.
    ```
    # systemctl status nginx
    ```

### Enabling firewall
> In this example, firewalld is used.

#### Install firewalld
```
# dnf install firewalld
```

#### Configure firewalld
1. Enable and start the service
    ```
    # systemctl enable --now firewalld
    ```

1. Add ports. If you use SSH, add it as well.
    ```
    # firewall-cmd --permanent --add-port={80,443}/tcp
    ```

1. Reload firewalld
    ```
    # firewall-cmd --complete-reload
    ```

## Configuring clients
Disable database compression (optional) and add a private mirror in the `freshclam.conf` file, so these options look something like this:
```
deathroll@fdesk:~ % egrep -i '^(private|compress)' /usr/local/etc/freshclam.conf
CompressLocalDatabase no
PrivateMirror https://cvd.deathroll.internal
deathroll@fdesk:~ % 
```

# The result
After the setup is complete, what we have is a Fedora 37 container with a local Tor-based SOCKS5 proxy that is used for updating the ClamAV database. On top of this, we have an NGINX web server that acts as a private mirror that allows clients to list and download database files for ClamAV. This web server redirects all requests to HTTPS and denies requests to any host different from the server's FQDN.

{% collapsible Deny a request to an IP address %}
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9ld0d8k287sjjap09zt5.png)
{% endcollapsible %}

{% collapsible Directory listing %}
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p0r01ilvs03fyqk5w35p.png)
{% endcollapsible %}

{% collapsible Update the ClamAV database on a client %}
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a76in1x522t6eavbhxq3.png)
{% endcollapsible %}