Opt-In Software Blog

Software, dvelopment, and practical insights

Configure SPF, DKIM, and DMARC for a Linux Mail Server

Read in: English | Русский

If you need to send email from your own domain without using third-party SMTP services, you can rent a Linux VPS. Postfix or Exim are commonly used as the mail transfer agent (MTA).

Before buying a VPS, contact the hosting provider and ask: “Is outbound SMTP port 25 open?” Many hosting providers block it by default to prevent spam. If the port is blocked, sending email directly to remote mail servers may not be possible.


1. Preparation: Network and DNS Settings

To make sure your mail is delivered correctly, you need to configure several basic DNS records for your domain:

  1. Hostname. Set the server hostname to something like mail.yourdomain.com.

  2. A record. Point mail.yourdomain.com to the IP address of your VPS.

  3. MX record. Specify which server handles email for your domain. The record looks like this:

    @ MX 10 mail.yourdomain.com.
  4. PTR record (Reverse DNS). Maps the server’s IP address back to its hostname. It is configured in your VPS provider’s control panel rather than at your domain registrar.

    A PTR record is important for email delivery. Many mail servers check the reverse DNS record of the sending IP address, and a missing or inconsistent PTR record can result in messages being rejected or sent to spam.

    It is recommended to keep the forward and reverse DNS records consistent:

    1.2.3.4 → mail.yourdomain.com
    mail.yourdomain.com → 1.2.3.4

2. Configuring SPF

SPF is a DNS record that specifies which servers are authorized to send email on behalf of your domain.

Create a TXT record:

  • Record type: TXT

  • Host/Name: @ (or leave it empty, depending on your DNS provider)

  • Value:

    v=spf1 ip4:1.2.3.4 ~all

    Replace 1.2.3.4 with the IP address of your server.

What does ~all mean?

~all means SoftFail: messages sent from IP addresses not listed in the SPF record receive a soft-failure result. The receiving server decides how to handle such messages.

After testing your configuration, you can use -all if all legitimate sources that send email for your domain are listed in the SPF record.


3. Generating DKIM Keys with OpenSSL

DKIM is a digital signature mechanism that helps verify the authenticity of the sender and the integrity of an email. The server signs the message with a private key, and the recipient verifies the signature using the public key published in DNS.

To generate a key pair, connect to the server over SSH and run the following commands.

3.1. Generate a 2048-bit private key

openssl genrsa -out dkim.private 2048

3.2. Generate the public key

openssl rsa -in dkim.private -pubout -out dkim.public

3.3. Display the public key

cat dkim.public

The terminal will display something like:

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A...
-----END PUBLIC KEY-----

For the DNS record, take only the Base64-encoded content between the headers and combine it into a single line without spaces or line breaks.

Then create a TXT record named dkim._domainkey (dkim is the selector name).

  • Record type: TXT

  • Host/Name: dkim._domainkey

  • Value:

    v=DKIM1; k=rsa; p=YOUR_COPIED_PUBLIC_KEY

If your DNS provider supports splitting long TXT records into multiple strings, use that option for keys that do not fit into a single string.


4. Connecting DKIM to the Mail Server

4.1 Option A: If You Use Exim

In the Exim configuration file (the exact path depends on the distribution and installation method), find the transport responsible for sending mail to external servers. It is usually called remote_smtp.

Example configuration:

remote_smtp:
  driver = smtp
  dkim_domain = yourdomain.com
  dkim_selector = dkim
  dkim_private_key = /etc/exim/dkim.private
  dkim_canon = relaxed

Copy the private key to the location specified in the Exim configuration:

cp dkim.private /etc/exim/dkim.private

Give the user running Exim permission to read the private key. The username depends on the distribution. For example, some systems use exim, while Debian may use Debian-exim.

Example for a system using the exim user:

chown exim:exim /etc/exim/dkim.private
chmod 600 /etc/exim/dkim.private

Restart the service:

systemctl restart exim4
# or
systemctl restart exim

Make sure that the Exim user actually has access to the private key.


4.2 Option B: If You Use Postfix

Postfix uses the external OpenDKIM utility to add DKIM signatures to outgoing messages.

4.2.1. Install the packages

apt install opendkim opendkim-tools

4.2.2. Configure OpenDKIM

Open /etc/opendkim.conf and add or configure the following settings:

Syslog                 yes
RequiredHeaders        yes
Mode                   sv
SubDomains             no
Socket                 inet:8891@localhost
KeyTable               /etc/opendkim/KeyTable
SigningTable           /etc/opendkim/SigningTable
ExternalIgnoreList     /etc/opendkim/TrustedHosts
InternalHosts          /etc/opendkim/TrustedHosts

4.2.3. Connect OpenDKIM to Postfix

Add the following lines to the end of the main Postfix configuration file, /etc/postfix/main.cf:

milter_protocol = 6
milter_default_action = accept
smtpd_milters = inet:localhost:8891
non_smtpd_milters = inet:localhost:8891

4.2.4. Configure the OpenDKIM tables

This example uses a single domain and a single DKIM key. The key name in KeyTable is dkim._domainkey.yourdomain.com.

/etc/opendkim/KeyTable

This file maps the key name to the domain, selector, and private key path:

dkim._domainkey.yourdomain.com yourdomain.com:dkim:/etc/opendkim/keys/dkim.private

Here:

  • dkim._domainkey.yourdomain.com — the key name in the table.
  • yourdomain.com — the domain that will appear in the DKIM signature (d=).
  • dkim — the selector (s=).
  • /etc/opendkim/keys/dkim.private — the path to the private key.

/etc/opendkim/SigningTable

This file maps the sender domain to the key name from KeyTable:

yourdomain.com dkim._domainkey.yourdomain.com

If you need to use a pattern for sender addresses from the domain, you can specify:

*@yourdomain.com dkim._domainkey.yourdomain.com

The wildcard form requires the corresponding refile: table type to be configured in the SigningTable directive. This guide uses the simple domain-based mapping without refile.

/etc/opendkim/TrustedHosts

Add localhost and the IP address of your server:

127.0.0.1
localhost
1.2.3.4

4.2.5. Move the private key and restart the services

Create the directory for the keys:

mkdir -p /etc/opendkim/keys/

Move the private key:

mv dkim.private /etc/opendkim/keys/

Set the owner and permissions:

chown opendkim:opendkim /etc/opendkim/keys
chmod 700 /etc/opendkim/keys

chown opendkim:opendkim /etc/opendkim/keys/dkim.private
chmod 600 /etc/opendkim/keys/dkim.private

Restart the services:

systemctl restart opendkim postfix

Check their status:

systemctl status opendkim
systemctl status postfix

If necessary, check the OpenDKIM log:

journalctl -u opendkim -n 50 --no-pager

5. Configuring DMARC

DMARC is a mechanism that allows a domain to tell receiving mail servers how to handle messages that fail the required SPF and DKIM checks, including the alignment of the domains involved with the domain in the From header.

Create a TXT record:

  • Record type: TXT

  • Host/Name: _dmarc

  • Initial value:

    v=DMARC1; p=none; rua=mailto:admin@yourdomain.com

What does this mean?

  • p=none — monitoring mode. The domain asks the receiving server not to apply a special action to messages that fail DMARC. You can monitor the results and collect reports.
  • rua=mailto:... — the address to which mail services can send aggregate reports about authentication checks.
  • p=quarantine — asks the receiving server to treat messages that fail DMARC as suspicious, for example by placing them in the spam folder.
  • p=reject — asks the receiving server to reject messages that fail DMARC. The actual action is determined by the receiving server.

After testing and fixing the configuration, you can change the policy from p=none to p=quarantine or p=reject.


6. How to Check DNS Records from the Terminal

You can check whether the DNS settings have been updated using dig or nslookup.

Check SPF

dig yourdomain.com TXT +short

Or:

nslookup -type=TXT yourdomain.com

Check DKIM

The query uses the name of your selector:

dig dkim._domainkey.yourdomain.com TXT +short

Or:

nslookup -type=TXT dkim._domainkey.yourdomain.com

Check DMARC

dig _dmarc.yourdomain.com TXT +short

Or:

nslookup -type=TXT _dmarc.yourdomain.com

Check PTR (Reverse DNS)

dig -x 1.2.3.4 +short

Or:

nslookup 1.2.3.4

Check the server hostname:

hostname -f

Make sure that the hostname is consistent with the A and PTR records.

The recommended setup is:

Hostname: mail.yourdomain.com
A:        mail.yourdomain.com → 1.2.3.4
PTR:      1.2.3.4 → mail.yourdomain.com

The hostname used by the server and the reverse DNS record should be configured consistently.

How Windows OpenSSH Broke My Brain

Read in: English | Русский

There are errors you can fix with a two-second Google search, and then there are those that drain your time. A prime example is an SSH connection failure to a Windows Server, where the OpenSSH service drops the following logs into the Event Viewer:

sshd: error: get_user_token - unable to generate token on 2nd attempt for user administrator
sshd: fatal: ga_init, unable to resolve user administrator

These messages indicate that the Windows authentication subsystem either cannot recognize the user at the OS level or is unable to generate a security token for them. Everything seems properly configured, yet the server persistently drops the connection.

Where the Time Goes (False Trails)

If you start googling this error, 90% of the online advice will lead you down two main paths:

  1. Key File Permissions: You will be told to configure ACLs via icacls for the administrators_authorized_keys file located in C:\ProgramData\ssh, disable inheritance, and restrict access exclusively to SYSTEM and Administrators.
  2. The sshd_config File: You will be advised to comment out the Match Group administrators block at the very bottom of the config to force OpenSSH to read keys from the standard user profile (.ssh/authorized_keys).

I tried everything on that list. Permissions and configs were verified, services were restarted—yet the result was zero. The server stubbornly terminated the session immediately after successfully validating the key.

The Unobvious Answer

The root cause turned out to be trivial, yet hidden behind the specifics of how OpenSSH operates on Windows.
How does SSH work in Linux? If the key matches, you are in.
How does SSH work in Windows? Even if the key matches, the OpenSSH service (running under the SYSTEM account) needs to establish a fully functional Windows workspace session and generate a user security token using the system API (LsaLogonUser).
And that is exactly where Windows denies the request.
I ran a quick check in the server console:

net user Administrator

And there it was: Account Active: Locked.
As it turned out, the Administrator account was locked out! This was likely caused by routine brute-force attempts—bots kept hammering port 22, triggering the Windows password lockout policy, which “froze” the account. Meanwhile, the SSH server successfully verified and confirmed the key (since the file exists on the disk), but the Windows OS refused to generate a token for a locked user. Hence the completely cryptic log about being unable to generate token.

The 5-Second Fix

If you have run into the same issue, the solution is straightforward. Log into the server using another admin account (or log in locally) and unlock the Administrator.

Via PowerShell:

Unlock-LocalUser -Name "Administrator"

Via the good old CMD:

net user Administrator /active:yes

Immediately after, the connection issues disappear, and the client successfully gains SSH access to the server.

How to Quickly Get the Direct URL from a Tracking Link

Read in: English | Русский

Services, online stores, and other websites often use tracking links instead of direct links.

This is especially noticeable in emails: a link to a question, order, product, or another page first goes through an intermediate service that tracks the click and then redirects you to the actual URL.

But tracking links are not limited to email. You can also encounter them on forums, in comments, communities, and other services — basically anywhere the site owner wants to track clicks on external links.

Sometimes the actual URL is already embedded directly in one of the tracking link’s parameters. For example:

https://tracking.example.com/click?u=https%3A%2F%2Fexample.com%2Forders%2F12345&campaign=email

Here, the u parameter contains the destination URL:

https%3A%2F%2Fexample.com%2Forders%2F12345

You don’t have to copy and decode it manually. You can quickly extract the direct URL right in DevTools.

How to do it

  1. Copy the tracking link.
  2. Open DevTools by pressing F12.
  3. Go to the Console tab.
  4. Enter:
const url = new URL("https://tracking.example.com/click?u=https%3A%2F%2Fexample.com%2Forders%2F12345&campaign=email");
  1. Then retrieve the value of the parameter:
url.searchParams.get("u");

The Console will display the already decoded direct URL:

https://example.com/orders/12345

You can even click the link directly in the Console.

The URL object provides access to query parameters through searchParams, and URLSearchParams.get() returns the value of a specific parameter. Query parameter values are decoded automatically, so there is no need to call decodeURIComponent() separately.

Of course, the parameter name will not always be u. Depending on the service, it could be url, target, redirect, redirect_url, or something else.

The main thing is to inspect the tracking link and look for a parameter that contains the encoded destination URL.

It’s a simple way to get the direct link without opening the tracking URL and going through the intermediate service.

How to get the URL from a reddit email

Links in emails from reddit look something like this:

https://click.redditmail.com/CL0/https:%2F%2Fwww.reddit.com%2Fr%2Frust%2F%3F%2524deep_link=true%26correlation_id=70d9a...

To get the direct link:

  1. In DevTools, enter the following (replace it with the link from your email):
const sourceUrl="https://click.redditmail.com/CL0/https:%2F%2Fwww.reddit.com%2Fr%2Frust%2F%3F%2524deep_link=true%26correlation_id=70d9a..."
  1. Run the following to extract the direct link:
decodeURIComponent(sourceUrl.match(/(?<=\/)https?:.*?(?=\/\d\/|$)/)[0])

Tools for Digital Footprint Auditing

Read in: English | Русский

Tools for Digital Footprint Auditing

Modern anti-fraud systems (Akamai, Cloudflare, PerimeterX) have long moved past analyzing just IPs and Cookies. Today, identification relies on Browser Fingerprinting — evaluating canvas/WebGL rendering artifacts, audio subsystem entropy, JA3/JA4 network stack hashes, and JS prototype behaviors. 

Top 9 Browser Fingerprint Checkers

  • BrowserScan — A comprehensive scanner tracking over 50 parameters. It detects automation frameworks (Puppeteer/Selenium) and scores overall profile consistency (Human Score).
  • BrowserLeaks — The go-to tool for low-level analysis. It verifies JA3/JA4 TLS fingerprints, HTTP/2 characteristics, and provides raw Canvas/WebGL hashes.
  • CreepJS — An open-source checker designed to expose spoofing. It analyzes Prototype Pollution, hidden anomalies in JS objects, and calculates a baseline Trust Score.
  • Pixelscan — A tool focused on logical data consistency. It cross-references environment metrics like system language, browser timezone, and proxy geolocation to catch mismatches.
  • Iphey — A simulator modeled after fintech and ad-tech verification engines. It evaluates profiles based on Autonomous System (ASN) reputation and hardware parity.
  • Cover Your Tracks (EFF) — A project by the Electronic Frontier Foundation. It measures browser uniqueness in bits of entropy against a live database of real users.
  • AmIUnique — A statistics-driven utility. It displays the exact percentage of global users sharing your specific font lists or screen resolution.
  • Scrapfly Browser Fingerprint Test — A verification tool developed by a web scraping platform. It simulates popular bot-detection challenges and monitors runtime reactions to script injections.
  • Whoer.net — A quick network perimeter assessment tool. It identifies WebRTC IP leaks, references DNSBL blocklists, and detects potential proxy/VPN tunnels using MTU analysis.

Comparative Analysis

Tool Focus JS / API Analysis Depth Network Stack Inspection Key Metric / Feature
BrowserScan Full profile auditing High Medium Automated consistency scoring
BrowserLeaks Low-level APIs High High (JA3/JA4) Aspect-by-aspect vector testing
CreepJS Spoof & mask detection Extreme Minimum In-depth JS prototype tracking
Pixelscan Profile data integrity Medium Medium System vs. IP anomaly detection
Iphey Reputation scoring Medium High (ASN, Proxy) ASN trust and behavior validation
Cover Your Tracks Privacy & tracking defense Medium Minimum Entropy measurements in bits
AmIUnique Global metadata trends Medium Minimum Percentage comparison with real devices
Scrapfly Test Anti-bot mitigation High High (HTTP/2) Cloudflare / Akamai engine simulation
Whoer.net Network perimeter Basic High (MTU, DNSBL) Rapid real IP exposure testing

WebSocket Session API in ProxyMapService

Read in: English | Русский

Real-Time Traffic Monitoring: Implementing a WebSocket Subscription API in ProxyMapService

Web scraping applications frequently require real-time tracking of all currently downloading URLs.

However, achieving this in practice is far from trivial. For instance, if the target application is a web browser, capturing network activity typically requires globally overriding the built-in fetch and XHR (XMLHttpRequest) methods. The situation becomes significantly more complex if the web page contains iframe elements: you must dynamically inject fetch and XHR overrides into them as well, adding a heavy layer of complexity and fragility to the system. Custom injection scripts can easily conflict with Content Security Policies (CSP) or simply miss requests originating from isolated contexts. 

Instead of breaking frontend logic inside the browser and relying on complex script injections, a much more reliable approach is to leverage the proxy layer. To achieve this, an optional WebSocket Session API was implemented in ProxyMapService. It allows external applications to connect directly to the proxy, supply a URL filter, and instantly receive push notifications for all requested resources. 

Configuration and Connection Logistics

The entire mechanism is managed by the SessionAPI section in the appsettings.json configuration file. Here is how it looks by default: 

"SessionAPI": {
    "Enabled": false,
    "WebSocketsEnabled": false,
    "Domain": ""
}

As you can see, by default, the entire SessionAPI infrastructure, including WebSockets, is turned off, and the Domain property is left empty. 

Option 1. Working via Local Port (Empty Domain)

When Domain is empty, the control endpoints are accessed like a regular web server on one of the ports that ProxyMapService listens to for incoming connections. For example, if the proxy is running on port 5000: 

To fully isolate control traffic from proxied traffic, it is recommended to enable the API and define a special internal domain: 

"SessionAPI": {
    "Enabled": true,
    "WebSocketsEnabled": true,
    "Domain": "proxymapper"
}

In this mode, proxymapper acts as a dedicated system domain. When ProxyMapService detects that a client is trying to access the proxymapper host, it intercepts the request locally and handles it as a call to the SessionAPI instead of routing it out to the external network. 

Crucial Nuance for Command-Line Utilities (e.g., cURL):

If you are using curl to route traffic through the proxy, the socks4 protocol will not allow you to call the internal API at all. Furthermore, the socks5:// prefix must be replaced with socks5h:// (e.g., socks5h://127.0.0.1:5000). This ensures that curl does not attempt to resolve the proxymapper domain locally via your OS DNS, but instead delegates the name resolution directly to the proxy server itself. 

With this configuration, the WebSocket Session API endpoint will be accessible via: ws://proxymapper/session/ws

How It Works in Practice

Using the subscription mechanism is straightforward for a developer: 

  1. Establishing a Connection: The client opens a persistent WebSocket connection to ws://proxymapper/session/ws.
  2. Sending a Filter: Immediately after a successful connection, the client sends a text JSON message containing a subscribe action and a regular expression (Regex) matching the target URLs:

 

{
  "action": "subscribe",
  "pattern": "api\\.example\\.com/v1/.*"
}
  1. Subscription Confirmation and session_id: The server replies with a confirmation message acknowledging the registered filter, which includes vital execution context:

 

{
  "status": "subscribed",
  "session_id": ":5000"
}

What does session_id mean and how does traffic isolation work? 

  • session_id represents a unique session identifier. If your environment utilizes authentication via a Sticky Proxy, this string identifier is explicitly bound to the user upon authentication.
  • If a Sticky Proxy is not used, the session_id returns a colon followed by the incoming port number (e.g., :5000). This means that notifications are strictly isolated to the specific port through which the subscription was created.
  • Lifecycle Rule: The user is guaranteed to receive notifications for URLs downloaded strictly within the session that was active at the moment of subscription. If the proxy session changes, notifications for the old subscription will cease. Similarly, under port-based isolation (without sticky routing), you will never see traffic passing through another proxy port inside the WebSocket channel for port :5000.
  1. Receiving Notifications: When a downloaded URL matches your pattern, the proxy server dispatches a flat, structured event optimized for fast reading:

 

{
  "event_type": "url_matched",
  "timestamp": "2026-09-02T10:20:45.0328755Z",
  "session_id": ":5000",
  "method": "GET",
  "url": "https://api.example.com/v1/users/profile"
}

Testing Environment: Proxy WebSocket Control Panel

The test web panel is hosted in a dedicated project subdirectory: tests\test-websocket

Inside the folder lies a self-contained HTML file, index.html, which implements the diagnostic interface: 

<!-- Located at: \tests\test-websocket\index.html -->
<div class="card">
    <h2>Proxy WebSocket Control Panel</h2>
    <div class="form-group">
        <input type="text" id="wsUrl" value="ws://proxymapper/session/ws">
        <button onclick="toggleConnection()">Connect</button>
    </div>
    <div class="form-group">
        <input type="text" id="filterPattern" value="(google\.com|yandex\.ru)/.*">
        <button onclick="sendSubscription()">Set Filter</button>
    </div>
</div>

How to Run the Test and Verify Monitoring:

  1. Launch the configured ProxyMapService with WebSockets enabled and the domain set to proxymapper.
  2. Open the tests\test-websocket\index.html file in any modern browser whose traffic is being routed through your proxy.
  3. Click the Connect button, set your regex filter pattern, and watch live notifications populate the dark console UI with full request details instantly.

Conclusion

Shifting URL tracing logic out of fragile browser scripts and into the ProxyMapService layer provides 100% visibility into network requests. It no longer matters where a request originates—be it the main window, a heavy background iframe, or a hidden service worker—the proxy server reliably intercepts, filters based on the active session (or port), and safely delivers the event to the client via a WebSocket subscription without modifying a single line of original page code.

Search