Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,382 @@
---
title: Isolate a PostgreSQL Database Behind a Cloud vLAN on netcup
description: Learn how to connect two netcup vServers over a Cloud vLAN so PostgreSQL runs on a database server with no public IP, reachable only through a public edge server running pgAdmin.
level: [intermediate]
updated_at: 2026-07-01
slug: private-vlan-postgres-pgadmin-two-vservers
author_name: Kai Wester
author_url: https://github.com/kaiwester
author_image:
author_bio:
tags: [debian, postgresql, pgadmin, vlan, cloud-vlan, networking, nat, security, netcup]
netcup_product_url: https://www.netcup.com/en/server/vps
language: en
available_languages: [en, de]
---

# Introduction

Running a database directly on the public internet is one of the most common ways servers get compromised. Every open PostgreSQL port is a target for brute-force login attempts and exploits. The safest database server is one that has no public IP address at all.

In this tutorial we build exactly that. We use two netcup vServers connected by a **Cloud vLAN** (virtual Local Area Network - a private network that only your own servers can see):

- An **edge server** with a public IP address. It is small (1 core, 1 GB RAM), runs **pgAdmin** (a web-based administration tool for PostgreSQL), and acts as the single, hardened entry point to your setup. It also works as a **NAT gateway** (Network Address Translation - it lets the private server reach the internet through the edge server's public IP) so the database server can still download package updates.
- A **database server** with more resources (4 cores, 8 GB RAM) that runs **PostgreSQL** and has **no public IP address**. It is reachable only over the private Cloud vLAN.

The resulting architecture looks like this:

```
Internet
|
public IPv4/IPv6 (ufw: SSH + HTTPS)
|
+-----------------------+ Cloud vLAN 10.132.0.0/24
| EDGE (1 core/1 GB) | eth1: 10.132.0.10 -----+
| - pgAdmin4 (web) | |
| - NAT gateway | |
+-----------------------+ |
|
+-----------------------+ |
| DB (4 core/8 GB) | eth1: 10.132.0.20 -----+
| - PostgreSQL | (NO public IP)
| - default route -> 10.132.0.10 (NAT)
+-----------------------+
```

The reading time of this tutorial is about 15 minutes; implementation will take approximately 90 minutes.

All examples were tested on **Debian 13 (Trixie)** in July 2026 and can be adapted to other distributions with minor changes. Throughout the tutorial the two servers are referred to as **s-edge** (the public edge server) and **s-db** (the private database server). When a command must be run on both, it is stated explicitly; otherwise it belongs to the named server only.

The tutorial uses the example private IP addresses `10.132.0.10` (s-edge) and `10.132.0.20` (s-db), and the placeholder `<edge-public-ip>` for the edge server's public IP address. Replace these with your own values as you follow along.

# Requirements

- Two netcup vServers, both running Debian 13:
- **s-edge** - the edge server, 1 core / 1 GB RAM is sufficient, e.g. [VPS pico G11s](https://www.netcup.com/de/server/vps/vps-pico-g11s-12m-nue).
- **s-db** - the database server, 4 cores / 8 GB RAM as requested for a PostgreSQL workload, e.g. [VPS Lite 2 G12s](https://www.netcup.com/de/server/vps/vps-lite-2-g12s-3m).
- A [Cloud vLAN Free](https://www.netcup.com/de/erweiterungen-kvm/cloud-vlan-free-nue) (or higher) added to both servers.
- Access to your [Customer Control Panel (CCP)](https://www.customercontrolpanel.de) and [Server Control Panel (SCP)](https://www.servercontrolpanel.de).
- A user with root privileges (`sudo`) on both servers.

This tutorial assumes you have already ordered the products above and completed basic server hardening. If you have not yet secured your servers with SSH key authentication and a non-root user, follow the [First steps to secure your Linux server](https://community.netcup.com/en/tutorials/first-steps-to-protect-your-linux-server-against-common-attacks) tutorial **first**, on both servers. We build the firewall on top of that baseline here.

> **Info**
> A Cloud vLAN Free is a layer-2 private network. Any server you attach to the same vLAN can talk to any other on it, isolated from the public internet and from other netcup customers.

# Step 1 - Attach the Cloud vLAN in the SCP

First, give each server a second network interface that is connected to your Cloud vLAN. This is done entirely in the web-based Server Control Panel (SCP).

1. Log in to your [SCP](https://www.servercontrolpanel.de), either directly or via the autologin feature in your [CCP](https://www.customercontrolpanel.de).
2. Choose **s-edge** from the server list on the top left.
3. Navigate to the **Network** section in the left sidebar.
4. Choose **add Ethernet Interface** on the top right.
5. Select your Cloud vLAN and choose **virtio** as the driver.
6. Repeat steps 2–5 for **s-db**, then continue with Step 2.

After a reboot (or a network restart, covered below) each server has a new interface for the vLAN. On netcup Debian images this is usually `eth1`, but it may appear as `ens7` or similar. Verify the exact name with `ip addr` before continuing.

# Step 2 - Configure the vLAN interface

The new interface has no IP address yet. We assign static private addresses using the classic `ifupdown` network stack, which is used by netcup's Debian images.

On a minimal installation, `ifupdown` may not be present. Install it on **both servers** if needed:

```bash
sudo apt update
sudo apt install ifupdown
```

## Step 2.1 - Configure s-edge

On **s-edge**, create the vLAN configuration with your favorite text editor, e.g. `sudo nano /etc/network/interfaces.d/vlan.cfg`:

```
auto eth1
iface eth1 inet static
address 10.132.0.10
netmask 255.255.255.0
```

## Step 2.2 - Configure s-db

On **s-db**, create the same file but with the second address. Because s-db will have no public IP, it also needs the edge server as its **default gateway** (the route through which all internet-bound traffic leaves) and it needs DNS resolvers (which translate hostnames like `deb.debian.org` into IP addresses):

```
auto eth1
iface eth1 inet static
address 10.132.0.20
netmask 255.255.255.0
gateway 10.132.0.10
dns-nameservers 46.38.225.230 46.38.252.230
```

> **Info**
> `46.38.225.230` and `46.38.252.230` are netcup's public IPv4 DNS resolvers.

## Step 2.3 - Restart networking

Apply the configuration on **both servers**:

```bash
sudo systemctl restart networking
```

# Step 3 - Verify vLAN connectivity

Before going further, confirm the private network actually works.

On **s-edge**, check that the interface has its address:

```bash
ip addr show eth1
```

You should see `inet 10.132.0.10/24` on the interface. Now ping s-db over the vLAN:

```bash
ping 10.132.0.20 -c 5
```

```
PING 10.132.0.20 (10.132.0.20) 56(84) bytes of data.
64 bytes from 10.132.0.20: icmp_seq=1 ttl=64 time=0.450 ms
64 bytes from 10.132.0.20: icmp_seq=2 ttl=64 time=0.510 ms
...
--- 10.132.0.20 ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 104ms
```

Then run the reverse ping from **s-db** to `10.132.0.10` to confirm the connection works both ways. Once both directions succeed, the vLAN is ready.

# Step 4 - Make the edge server a NAT gateway

The database server has no public IP, so on its own it cannot reach the internet for package updates. We solve this by turning **s-edge** into a NAT gateway: it forwards s-db's outbound traffic and rewrites it to use its own public IP.

All commands in this step run on **s-edge**.

## Step 4.1 - Enable IP forwarding

By default Linux does not forward packets between interfaces. Enable it. Edit `/etc/sysctl.conf`, e.g. `sudo nano /etc/sysctl.conf`, and make sure this line is present and uncommented:

```
net.ipv4.ip_forward = 1
```

Apply it immediately:

```bash
sudo sysctl -p
```

## Step 4.2 - Configure NAT in ufw

We manage the firewall and NAT with **ufw** (Uncomplicated Firewall). First allow forwarded traffic. Edit `/etc/default/ufw`, e.g. `sudo nano /etc/default/ufw`, and change the forward policy:

```
DEFAULT_FORWARD_POLICY="ACCEPT"
```

Next add the NAT masquerade rule. Edit `/etc/ufw/before.rules`, e.g. `sudo nano /etc/ufw/before.rules`, and add the following block **at the very top of the file**, before the existing `*filter` line. Replace `eth0` with your public interface and `eth1` with your vLAN interface if they differ:

```
*nat
:POSTROUTING ACCEPT [0:0]
# Masquerade traffic from the Cloud vLAN going out via the public interface
-A POSTROUTING -s 10.132.0.0/24 -o eth0 -j MASQUERADE
COMMIT
```

Reload ufw to apply (if ufw is not enabled yet, that happens in Step 7 - the rule takes effect once enabled):

```bash
sudo ufw reload
```

## Step 4.3 - Verify internet access from s-db

On **s-db**, confirm it can now reach the internet through the gateway:

```bash
ping 1.1.1.1 -c 3
sudo apt update
```

Both should succeed even though s-db still has no meaningful public presence. If `apt update` hangs on DNS, re-check the `dns-nameservers` line from Step 2.2.

# Step 5 - Install and configure PostgreSQL

All commands in this step run on **s-db**.

## Step 5.1 - Install PostgreSQL

```bash
sudo apt update
sudo apt install postgresql
```

## Step 5.2 - Bind PostgreSQL to the vLAN only

By default PostgreSQL listens only on `localhost`. We want it to listen on the **private vLAN address only** - never on `*` (all interfaces) and never on a public IP. This single setting is what keeps the database off any public network.

Find your config file location:

```bash
sudo -u postgres psql -c 'SHOW config_file;'
```

On Debian 13 this is typically `/etc/postgresql/17/main/postgresql.conf`. Edit it and set:

```
listen_addresses = '10.132.0.20'
```

## Step 5.3 - Allow the edge server to connect

Edit `pg_hba.conf` in the same directory (e.g. `/etc/postgresql/17/main/pg_hba.conf`). This file (`pg_hba` stands for "host-based authentication") controls which clients may connect. Add a line permitting **only** the edge server's vLAN address, using encrypted password authentication:

```
# TYPE DATABASE USER ADDRESS METHOD
host all all 10.132.0.10/32 scram-sha-256
```

## Step 5.4 - Create a database and role

Create an application role (user) and a database for it. Replace the example password with a strong one:

```bash
sudo -u postgres psql
```

```sql
CREATE ROLE appuser WITH LOGIN PASSWORD 'changeMe.aHighlyComplexPassword';
CREATE DATABASE appdb OWNER appuser;
\q
```

## Step 5.5 - Restart PostgreSQL

```bash
sudo systemctl restart postgresql
```

Confirm PostgreSQL is now listening on the vLAN address:

```bash
sudo ss -tlnp | grep 5432
```

You should see it bound to `10.132.0.20:5432` and nothing else.

# Step 6 - Install pgAdmin in server mode

pgAdmin gives you a web interface to manage PostgreSQL. We install it natively in **server mode** (running behind the Apache web server), which is lighter on the 1 GB edge server than a container.

All commands in this step run on **s-edge**.

## Step 6.1 - Add the pgAdmin repository and install

```bash
# Import the repository signing key
curl -fsS https://www.pgadmin.org/static/packages_pgadmin_org.pub | sudo gpg --dearmor -o /usr/share/keyrings/packages-pgadmin-org.gpg

# Add the repository (uses the Debian codename automatically)
sudo sh -c 'echo "deb [signed-by=/usr/share/keyrings/packages-pgadmin-org.gpg] https://ftp.postgresql.org/pub/pgadmin/pgadmin4/apt/$(lsb_release -cs) pgadmin4 main" > /etc/apt/sources.list.d/pgadmin4.list'

sudo apt update
sudo apt install pgadmin4-web
```

> **Info**
> If no pgAdmin package exists yet for the Debian 13 codename `trixie`, replace `$(lsb_release -cs)` with the most recent supported codename (for example `bookworm`) in the command above.

## Step 6.2 - Configure the web interface

Run the setup script. It configures Apache and asks you to create a pgAdmin login (email and password):

```bash
sudo /usr/pgadmin4/bin/setup-web.sh
```

You can now open pgAdmin in your browser at:

```
https://<edge-public-ip>/pgadmin4
```

## Step 6.3 - Connect to PostgreSQL over the vLAN

Log in to pgAdmin, then add the database server as a new connection:

- **Host name/address:** `10.132.0.20` (the private vLAN address of s-db)
- **Port:** `5432`
- **Username:** `appuser`
- **Password:** the password from Step 5.4

For a detailed walkthrough of the pgAdmin connection form, see the [Installing PostgreSQL and pgAdmin with Docker](https://community.netcup.com/en/tutorials/how-to-install-postgres-and-pgadmin-with-docker-and-connect-pgadmin-with-postgres) tutorial. The connection travels entirely over the private vLAN - it never touches the public internet.

# Step 7 - Firewall both servers with ufw

Now lock both servers down. We assume `ufw` is installed (it is part of the baseline hardening tutorial linked in the requirements); if not, `sudo apt install ufw` on both.

## Step 7.1 - Firewall the edge server

On **s-edge**, deny all incoming traffic by default, then allow only SSH and the web ports (80 for the Let's Encrypt HTTP challenge, 443 for pgAdmin over HTTPS):

```bash
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
```

## Step 7.2 - Firewall the database server

On **s-db**, deny everything incoming, then allow **only the edge server** to reach PostgreSQL and SSH over the vLAN:

```bash
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow from 10.132.0.10 to any port 22 proto tcp
sudo ufw allow from 10.132.0.10 to any port 5432 proto tcp
sudo ufw enable
```

> **Tip**
> Because SSH to s-db is now allowed only from the edge server, you reach s-db through the edge as a jump host: `ssh -J operator@<edge-public-ip> operator@10.132.0.20`.

# Conclusion

You now have a PostgreSQL database server with **no public IP address**, reachable only across a private Cloud vLAN. All administration flows through a single, hardened edge server: pgAdmin for the database, and an SSH jump host for the shell. The edge server also provides the database with internet access for updates via NAT. The attack surface of the database has been reduced from "the entire internet" to "one server you control."

Some suggested next steps:

- Enable TLS between pgAdmin and PostgreSQL for encryption on the vLAN as well.
- Install [Fail2Ban](https://community.netcup.com/en/tutorials/how-to-install-and-configure-fail2ban-on-a-debian-server) on the edge server to block brute-force SSH attempts.
- Set up automated PostgreSQL backups on the database server.

# Licence

[MIT](https://github.com/netcup-community/community-tutorials/blob/main/LICENSE)

Copyright (c) 2026 netcup

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicence, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

# Contributor's Certificate of Origin

By making a contribution to this project, I certify that:

1. The contribution was created in whole or in part by me and I have the right to submit it under the licence indicated in the file; or

2. The contribution is based upon previous work that, to the best of my knowledge, is covered under an appropriate licence and I have the right under that licence to submit that work with modifications, whether created in whole or in part by me, under the same licence (unless I am permitted to submit under a different licence), as indicated in the file; or

3. The contribution was provided directly to me by some other person who certified (a), (b) or (c) and I have not modified it.

4. I understand and agree that this project and the contribution are public and that a record of the contribution (including all personal information I submit with it, including my sign-off) is maintained indefinitely and may be redistributed consistent with this project or the licence(s) involved.
Loading
Loading