Skip to main content

· 7 min read
Moazzem Hossen

iptables

iptables is a powerful firewall management tool for Linux-based systems. It allows you to control incoming and outgoing network traffic, making it a vital component for securing and managing your server or network. Here's a quick overview of what you need to know:

Key Concepts

  • Rules: iptables operates using a set of predefined rules. Each rule defines how packets should be handled, including whether to allow, deny, or modify them.

  • Tables: iptables has three main tables: filter, nat, and mangle. Each table is responsible for different types of packet manipulation.

  • Chains: Within each table, there are chains like INPUT, OUTPUT, and FORWARD, which specify when the rules should be applied.

Basic Operations

  • Allow Incoming Traffic: To allow incoming traffic on a specific port (e.g., port 80 for HTTP):

    iptables -A INPUT -p tcp --dport 80 -j ACCEPT
  • Block Incoming Traffic: To block incoming traffic on a specific port:

    iptables -A INPUT -p tcp --dport 22 -j DROP
  • Port Forwarding: Redirect incoming traffic on a specific port to an internal IP and port:

    iptables -t nat -A PREROUTING -p tcp --dport 80 -j DNAT --to-destination [internal-ip]:[internal-port]

Save and Restore Rules

  • Save Rules: To save your iptables rules so they persist across reboots:

    sudo iptables-save > /etc/iptables/rules.v4
  • Restore Rules: To load saved rules:

    sudo iptables-restore < /etc/iptables/rules.v4

Additional Modules

  • iptables can be extended with additional modules like iptables-persistent for automatic rule loading at boot or iptables-persistent6 for IPv6.

  • You can also use graphical frontends like ufw (Uncomplicated Firewall) or firewalld for easier rule management.

NAT Routing

Understanding Network Address Translation (NAT) is crucial in the realm of networking. It allows the modification of network address information in packet headers while in transit. iptables provides a robust platform for configuring NAT on Linux systems. This guide delves into various NAT operations using iptables, guiding you from basic to advanced configurations.

Understanding NAT Types

  • SNAT (Source NAT): Alters the source address of outbound packets. Ideal for changing the sender's address, especially for traffic leaving your network.
  • DNAT (Destination NAT): Changes the destination address of inbound packets. Used for redirecting incoming traffic to a specific internal host/port.
  • MASQUERADE: A special SNAT type useful for dynamic IP addresses, like those from DHCP.

Setting Up for NAT

Ensure your system is ready for NAT configurations:

  1. Enable IP Forwarding: This allows your Linux system to forward packets between interfaces.

    sudo sysctl -w net.ipv4.ip_forward=1
  2. Activate the New Settings: Make the changes effective immediately.

    sudo sysctl -p

Implementing NAT Rules

Source NAT (SNAT)

Change the source IP of outgoing traffic:

  • Basic SNAT: Modify the source IP for traffic exiting a specific interface.

    sudo iptables -t nat -A POSTROUTING -s [source-ip] -o [out-interface] -j SNAT --to-source [new-source-ip]

Destination NAT (DNAT)

Redirect incoming traffic to a different internal host/port:

  • Port Forwarding: Redirect traffic hitting your public IP on a specific port to an internal IP and port.

    sudo iptables -t nat -A PREROUTING -p tcp --dport [public-port] -j DNAT --to-destination [internal-ip]:[internal-port]

MASQUERADE

Dynamic IP handling, ideal for home routers or DHCP clients:

  • Internet Connection Sharing: Allow internal network clients to access the Internet through your Linux gateway.

    sudo iptables -t nat -A POSTROUTING -o [out-interface] -j MASQUERADE

Whether it's sharing an internet connection or setting up a secure internal network, iptables NAT functionality is a powerful tool in network configuration.

The MASQUERADE target in iptables is a specialized form of Source NAT (SNAT) designed for situations where your outbound interface has a dynamically assigned IP address, such as those obtained via DHCP. This is commonly used in scenarios like home networks, where your router/gateway receives a dynamic IP from your ISP.

  • Why MASQUERADE? In traditional SNAT, the new source IP is fixed and explicitly specified in the rule. However, if your gateway's external IP address is dynamically assigned and can change (like in residential broadband connections), setting up a standard SNAT rule with a fixed IP is impractical. MASQUERADE automatically handles this by using the current IP of the outgoing interface as the source IP for NAT.

  • How it works: When MASQUERADE is applied, it dynamically alters the source IP of outbound packets to match the IP of the interface they're leaving from. If that interface's IP changes over time (as is the case with dynamic IPs), MASQUERADE adapts and uses the new IP automatically, without needing any rule updates.

  • Common Use Case - Internet Sharing: A typical use case for MASQUERADE is in a Linux-based home router or gateway, where the internal network needs to access the internet, but the external IP of the router is not static. By applying MASQUERADE to the outbound traffic, all internal clients can seamlessly access external networks through the gateway, regardless of changes in the gateway's external IP.

  • MASQUERADE Example: To enable MASQUERADE for outbound traffic on a dynamically assigned external interface (e.g., eth0):

    sudo iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE

This rule tells the system: "For all outbound packets on eth0, change their source IP to match eth0's current IP." It's an essential tool for managing NAT in dynamic IP environments and is widely used in both home and small business networks.


In iptables, the terms POSTROUTING and PREROUTING refer to specific chains within the NAT (Network Address Translation) table. These chains are crucial for manipulating packets as they enter and leave the network system, especially in routing and forwarding scenarios. Here's a brief explanation of these and other similar chains:

  • PREROUTING: This chain is used for altering incoming packets before they are routed. It's often used for redirecting incoming traffic to different destinations within your local network through DNAT (Destination Network Address Translation).

  • POSTROUTING: This chain is used for modifying packets after they have been routed. It's commonly used for masquerading (a form of source NAT), where the source IP addresses are changed to the firewall's IP address as they leave the network.

In addition to these, there are other chains in the NAT table and other tables of iptables. While PREROUTING and POSTROUTING are specific to the NAT table, there are equivalent chains in other tables for different purposes:

  • INPUT: Found in the filter table, it's used to control the behavior of incoming packets destined for the local system.

  • OUTPUT: Also in the filter table, this chain is for packets generated by the local system and going out.

  • FORWARD: Part of the filter table, it's used for packets that are routed through the local system, useful in scenarios where your system is acting as a router.

  • MANGLE: This is a table in itself and is used for specialized packet alteration. It has chains like PREROUTING, OUTPUT, etc., similar to the NAT table but for different packet manipulation purposes.

Understanding these chains and tables is essential for effective network management and security using iptables. Each serves a unique role in how packets are processed and routed through your system.

· 2 min read
Moazzem Hossen
#!/bin/bash

# Create network namespaces
ip netns add wg1
ip netns add wg2

# Create a veth pair
ip link add veth-wg1 type veth peer name veth-wg2

# Attach the veth interfaces to the namespaces
ip link set veth-wg1 netns wg1
ip link set veth-wg2 netns wg2

# Assign IP addresses to each veth interface in their respective namespaces
ip -n wg1 addr add 192.168.15.1/24 dev veth-wg1
ip -n wg2 addr add 192.168.15.2/24 dev veth-wg2

# Bring up the loopback interfaces
ip -n wg1 link set lo up
ip -n wg2 link set lo up

# Bring up the veth interfaces
ip -n wg1 link set veth-wg1 up
ip -n wg2 link set veth-wg2 up

# Add routes in the namespaces
ip -n wg1 route add 192.168.15.2 dev veth-wg1
ip -n wg2 route add 192.168.15.1 dev veth-wg2

# Test connectivity with ping
ip netns exec wg1 ping -c 4 192.168.15.2

linux bridge

#!/bin/bash
ip netns add wg1
ip netns add wg2

ip link add name v-net-0 type bridge
ip link set dev v-net-0 up

ip link add veth-wg1 type veth peer name veth-wg1-br
ip link set veth-wg1 netns wg1
ip link set veth-wg1-br master v-net-0

ip link add veth-wg2 type veth peer name veth-wg2-br
ip link set veth-wg2 netns wg2
ip link set veth-wg2-br master v-net-0

ip -n wg1 addr add 192.168.15.1/24 dev veth-wg1
ip -n wg1 link set veth-wg1 up
ip -n wg1 link set lo up

ip -n wg2 addr add 192.168.15.2/24 dev veth-wg2
ip -n wg2 link set veth-wg2 up
ip -n wg2 link set lo up

ip link set veth-wg1-br up
ip link set veth-wg2-br up

ip netns exec wg1 ping -c 4 192.168.15.2

connecting to and from outside

# sysctl net.ipv4.ip_forward
# sysctl -w net.ipv4.ip_forward=1
sed -i '/^#net.ipv4.ip_forward=1/s/^#//' /etc/sysctl.conf
sysctl -p

ip link add name br0 type bridge
ip addr add 192.168.15.5/24 dev br0
ip link set br0 up


iptables -t nat -A POSTROUTING -s 192.168.15.0/24 -j MASQUERADE
iptables -t nat -A PREROUTING -p tcp --dport 80 -j DNAT --to-destination 192.168.15.1:80

ip netns exec wg1 ip route add default via 192.168.15.5

# ping from wg1 namespace via br0 (192.168.15.5) to outside
ip netns exec wg1 ping 192.168.121.57

· 2 min read
Moazzem Hossen

In relational databases like Postgres, a relationship between entities, i.e., tables, is implemented by storing the primary key of one entity as a pointer or "foreign key" in the table of another entity.

One-to-one

A one to one relationship between two entities occurs when a single record in one entity is related to only one record in another entity, and vice versa.

-- Already created by Keycloak
CREATE TABLE IF NOT EXISTS auth.user_entity (
id VARCHAR(36) PRIMARY KEY
-- other fields
);

CREATE TABLE IF NOT EXISTS user_profiles (
id VARCHAR(36) PRIMARY KEY REFERENCES auth.user_entity(id)
);

One-to-many

A one to many relationship is where a single record in one entity can be associated with one or more records in another entity. This is the most common relationship type. In the parent table, use a primary key, id which is refernced using a foreign key, author_id, in the child table.

CREATE TABLE authors (
id INT GENERATED BY DEFAULT AS IDENTITY (START WITH 1001) PRIMARY KEY,
-- first_name TEXT NOT NULL,
-- last_name TEXT NOT NULL
);

CREATE TABLE books (
id INT GENERATED BY DEFAULT AS IDENTITY (START WITH 10001) PRIMARY KEY,
-- title TEXT NOT NULL,
author_id INT NOT NULL REFERENCES authors(id) ON DELETE CASCADE,
);

Many-to-many

A many to many relationship occurs when multiple records in one entity are related to multiple records in another entity. This type of relationship requires a junction table to manage the associations between the two entities. Create a third table (junction table) that holds foreign keys that reference the primary keys in the two entities being linked. This junction table effectively breaks down the many to many relationship into two 1 to many relationships.

CREATE TABLE students (
id INT GENERATED BY DEFAULT AS IDENTITY (START WITH 100001) PRIMARY KEY,
name TEXT NOT NULL
);

CREATE TABLE courses (
id INT GENERATED BY DEFAULT AS IDENTITY (START WITH 1001) PRIMARY KEY,
title TEXT NOT NULL
);

CREATE TABLE enrollments (
student_id INT NOT NULL REFERENCES students(id) ON DELETE CASCADE,
course_id INT NOT NULL REFERENCES courses(id) ON DELETE CASCADE,
enrollment_date DATE,
PRIMARY KEY (student_id, course_id)
);

· One min read
Moazzem Hossen
CREATE TABLE ddl_logs (
log_id SERIAL PRIMARY KEY,
event_type TEXT,
object_type TEXT,
schema_name TEXT,
object_name TEXT,
ddl_command TEXT,
executed_by TEXT,
execution_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE OR REPLACE FUNCTION log_ddl_event()
RETURNS event_trigger AS $$
DECLARE
r RECORD;
BEGIN
FOR r IN SELECT * FROM pg_event_trigger_ddl_commands() LOOP
INSERT INTO ddl_logs (event_type, object_type, schema_name, object_name, ddl_command, executed_by)
VALUES (r.command_tag, r.object_type, r.schema_name, r.object_identity, r.command, current_user);
END LOOP;
END;
$$ LANGUAGE plpgsql;
CREATE EVENT TRIGGER track_ddl_changes ON ddl_command_end
EXECUTE FUNCTION log_ddl_event();

· 4 min read
Moazzem Hossen

Let's consider the below tables for a simple blog application.

CREATE TABLE IF NOT EXISTS authors (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(255) NOT NULL UNIQUE CHECK (email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$'),
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE IF NOT EXISTS posts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
author_id UUID NOT NULL REFERENCES authors(id),
title TEXT NOT NULL,
body TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);

We want to restrict the access to the authors and posts tables based on the user's role and the ownership of the data.

GRANT command grants

  • membership in a role, or
  • privileges on a database object (table, etc).

In addition to GRANT, tables can have row security policies that restrict, on a per-user basis, which rows can be returned by normal queries or inserted, updated, or deleted by data modification commands. This feature is known as Row-Level Security (RLS).

We will use the webui role for the authenticated web users and the anon role for the anonymous users. We'll further restrict the access to the authors table based on the user's id from the JWT token issued by the authentication server, Keycloak.

  • Grant privileges on the tables to the anon (anonymous) role and webui role. The webui role will be used for the authenticated web users.
GRANT SELECT ON posts TO anon;
GRANT SELECT ON authors TO anon;
GRANT ALL PRIVILEGES ON authors TO webui;
GRANT ALL PRIVILEGES ON posts TO webui;
-- Or, grant specific privileges
-- GRANT SELECT (id, username) ON authors TO anon;
-- GRANT SELECT, INSERT, UPDATE, DELETE ON posts TO webui;
-- GRANT SELECT, INSERT, UPDATE, DELETE ON authors TO webui;
  • Insert some mock data into the tables.
INSERT INTO authors (username, email) VALUES
('john_doe', '[email protected]'),
('jane_smith', '[email protected]'),
('alex_jones', '[email protected]');

-- Insert 2 posts for each author
DO $$
DECLARE
author_record RECORD;
BEGIN
FOR author_record IN SELECT id, username FROM authors LOOP
INSERT INTO posts (author_id, title, body) VALUES
(author_record.id, 'First Post Title by ' || author_record.username, 'First post body by ' || author_record.username),
(author_record.id, 'Second Post Title by ' || author_record.username, 'Second post body by ' || author_record.username);
END LOOP;
END $$;

Row Level Security Policies

  • Allow authors to SELECT their own profile. policy name select_self is arbitrary.
CREATE POLICY select_self ON authors
FOR SELECT
USING (id = auth.user_jwt_sub());
ALTER POLICY select_self ON authors TO webui;

USING (id = auth.user_jwt_sub()): A user can only attempt to update rows where their id matches the id decoded from their JWT token (auth.user_jwt_sub()).

  • Allow users to UPDATE their own profile, except for username
CREATE POLICY update_self ON authors
FOR UPDATE
USING (id = auth.user_jwt_sub())
WITH CHECK (id = auth.user_jwt_sub());
ALTER POLICY update_self ON authors TO webui;

WITH CHECK (id = auth.user_jwt_sub()): After the update, the id of the row must still match the user's id from their JWT token. This prevents users from updating records that do not belong to them.

  • Allow everyone to SELECT all posts
CREATE POLICY select_anon ON posts
FOR SELECT
USING (true);
ALTER POLICY select_anon ON posts TO anon, webui;
  • Allow authenticated users to INSERT posts
CREATE POLICY insert_authn ON posts
FOR INSERT
WITH CHECK (author_id = auth.user_jwt_sub());
ALTER POLICY insert_authn ON posts TO webui;
  • Allow the post owner to UPDATE their own posts
CREATE POLICY update_own ON posts
FOR UPDATE
USING (author_id = auth.user_jwt_sub())
WITH CHECK (author_id = auth.user_jwt_sub());
ALTER POLICY update_own ON posts TO webui;
  • Allow the post owner to DELETE their own posts
CREATE POLICY delete_own ON posts
FOR DELETE
USING (author_id = auth.user_jwt_sub());
ALTER POLICY delete_own ON posts TO webui;

For authenticated requests, PostgREST automatically sets the request.jwt.claims configuration variable based on the decoded JWT provided in the request. This can be used to implement RLS policies, as in auth.user_jwt_sub() utility which returns the sub claim from the JWT token as a UUID.

CREATE OR REPLACE FUNCTION auth.user_jwt_sub() RETURNS UUID AS $$
DECLARE
jwt_claim_sub UUID;
BEGIN
SELECT (current_setting('request.jwt.claims', true)::json->>'sub')::UUID INTO jwt_claim_sub;
RETURN jwt_claim_sub;
END;
$$ LANGUAGE plpgsql STABLE;

For debugging etc purposes one can manually set request.jwt.claims configuration variable to the decoded JWT claims.

SET request.jwt.claims TO 'jwt.claims';

· One min read
Moazzem Hossen
  • Install Rust and WebAssembly
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
rustup update
cargo install cargo-generate
rustup target add wasm32-unknown-unknown
cargo install wasm-pack
  • Create a new project
cargo new rust-wasm-helloworld --lib
cd rust-wasm-helloworld
  • Add dependencies by updating Cargo.toml
[package]
name = "rust-wasm-helloworld"
version = "0.1.0"
edition = "2021"

[dependencies]
wasm-bindgen = "0.2"
web-sys = { version = "0.3", features = ["console"] }

[lib]
crate-type = ["cdylib", "rlib"]
  • Add the following code to src/lib.rs
use wasm_bindgen::prelude::*;

// Function to be called from JavaScript, prints "Hello, world!" to the console
#[wasm_bindgen]
pub fn print_hello() {
web_sys::console::log_1(&"Hello, world!".into());
}
  • Build the project
wasm-pack build --target web
  • Create an index.html and include the generated JavaScript file, rust_wasm_helloworld.js
<!DOCTYPE html>
<html>
<head>
<title>Wasm Test</title>
<script type="module">
import init, { print_hello } from './pkg/rust_wasm_helloworld.js';

async function run() {
await init();
print_hello();
}

run();
</script>
</head>
<body>
<h1>Check the console for a message</h1>
</body>
</html>