Skip to content

UNIX‐groups, security and ssh

sucks less edited this page Sep 27, 2025 · 28 revisions

Enhancing Security Using Unix Groups

Creating a Dedicated Group and User for Monero Named Pipes

1. Create a new user and group for mnp:

sudo useradd mnp
sudo groupadd mnp-group
sudo usermod --append -G mnp-group mnp

2. Refresh your login permissions:

su -l mnp

Setting Permissions on mnp Commands

1. Assign ownership and permissions to mnp commands:

sudo chown mnp:mnp-group /usr/local/bin/mnp /usr/local/bin/mnpd /usr/local/bin/mnp-payment
sudo chmod 750 /usr/local/bin/mnp /usr/local/mnpd /usr/local/bin/mnp-payment

Add the following configuration to the ~/.mnp.ini file:

[cfg]                           ;workdir configuration
workdir = /tmp/mywallet         ;wallet working directory
mode = rwx------                ;permission of workdir rwxrwxrwx
pipe = rw-------                ;permission of pipes rwxrwxrwx

2. Initialize the mnp environment:

sg mnp-group -c "mnp --init"

Note: The sg command is only called once. Subsequent calls are not necessary as long as /tmp/mywallet exists. The mnp --init command also sets the S_ISGID flag, ensuring that new directories inherit the group ownership from their parent group ID.

Secure Shell (ssh)

Using mnp-payment on a remote host:

#return integrated address
openssl rand --hex 8 | ssh -T mnp@mywallethost "mnp-payment --amount 5555" 

Using mnp on a remote host

#!/bin/bash
REMOTE_USER="mnp"
REMOTE_HOST="mywallet_host"
REMOTE_DIR="/tmp/mywallet/transactions"

known=""

# Function to handle reading from a pipe
read_pipe() {
    local file="$1"
    local amount

    # Set timeout for cat command (125 minutes)
    if ! amount=$(ssh "$REMOTE_USER@$REMOTE_HOST" "timeout 125m cat '${REMOTE_DIR}/${file}'"); then
        # Handle timeout - write to syslog and exit
        echo "Timeout occurred while reading from $file" >&2
        logger "Timeout occurred while reading from $file"
        exit 1
    fi

    echo "Received from $file: $amount"
}

while true; do
    # Find all named pipes in the remote directory
    remote_fifos=$(ssh "$REMOTE_USER@$REMOTE_HOST" "find '$REMOTE_DIR' -type p 2>/dev/null")

    for fifo in $remote_fifos; do
        file=$(basename "$fifo")
        if [[ ! "$known" =~ "$file" ]]; then
            echo "New transaction detected: $file"
            known="$known $file"
            (
                read_pipe "$file"
            ) &
        fi
    done
    sleep 1
done

ssh tunnel (autossh)

When using autossh, the POS/Webserver does not connect to monero-wallet-rpc directly over the network. Instead, it first establishes a persistent encrypted tunnel to the Wallet-Host.

Once the tunnel is active:

  1. mnp-payment and mnpd run on the POS/Webserver as usual.

  2. They send their requests to localhost:18083, which is forwarded securely to the Wallet-Host.

  3. On the Wallet-Host, a hook script can be used to start mnp (if it isn’t running yet) whenever a new request arrives.

This approach has three main benefits:

  • Security: RPC is never exposed to the public network.

  • Automation: The hook script can automatically manage mnp’s lifecycle (start/stop).

  • Resilience: With autossh, the tunnel is re-established automatically if the connection drops.

sudo usermod -s /usr/sbin/nologin mnp

+----------------+                           +----------------------+
|                |     Encrypted SSH         |                      |
|   Webserver/   |==========================>|     Wallet-Host      |
|      POS       |   Tunnel (autossh/ssh)    |                      |
|  +---------+   |                           |  +----------------+  |
|  |   mnp-  |---|--> localhost:18083 ------>|->| monero-wallet- |  |
|  | payment |   |   (forwarded port)        |  |      rpc       |  |
|  +---------+   |                           |  | bind 127.0.0.1 |  |
|  +---------+   |                           |  |                |  |
|  |   mnpd  |---|--> localhost:18083 ------>|->|                |  |
|  |  daemon |   |   (forwarded port)        |  |                |  |
|  +---------+   |                           |  |                |  |
|  +---------+   |                           |  |                |  |
|  |   mnp   |---|--> localhost:18083 ------>|->|                |  |
|  | (client)|   |   (forwarded port)        |  |                |  |
|  +----+----+   |                           |  +--------+-------+  |
|       | launch |                           | tx-notify |          |
|  +----^-----+  |                           |     +-----+------+   |
|  |  mnp-    |<-|<------webserver:20-----<--|-----| hook       |   |
|  | wrappper |  |    using dedicated key    |     | notify-mnp |   |
|  +----------+  |          (ssh)            |     +------------+   |
|                |                           |                      |
+----------------+                           +----------------------+

Legend:

  • On the Webserver, mnp always connects to 127.0.0.1:18083 locally.
  • autossh/ssh forwards this local port through an encrypted SSH tunnel.
  • On the Wallet-Host it reaches 127.0.0.1:18083, where monero-wallet-rpc is listening with --rpc-login user:pass.
  • The RPC port is bound only to localhost (127.0.0.1) on the Wallet-Host, so it is not exposed to the outside network.

Security best practices 0 (Wallet Host)

Security Note:
An SSH tunnel by itself does not grant a shell on the wallet host.
When started with ssh -N -L ... (or via autossh), it only forwards ports and does not run any commands or open a terminal.

To prevent accidental shell access entirely, configure the mnp user with /usr/sbin/nologin and restrict the SSH key in
~/.ssh/authorized_keys to port forwarding only, for example:

command="echo 'Port forwarding only'; exit 1",no-agent-forwarding,no-X11-forwarding,no-pty ssh-ed25519 AAAAC3...

This way, even if someone tries to open a shell with that key, the connection will be refused.
You still need a valid private key on the POS/Webserver to establish the tunnel, so protect it with a strong passphrase.

hook-script to launch mnp from monero-wallet-rpc

#!/usr/bin/env bash
#
# notify_mnp.sh
#
# This script is called by monero-wallet-rpc via --notify-at.
# Argument: %s = TXID (transaction hash)
#
# It connects to the webserver via SSH and executes
# `mnp --confirmation n <TXID>` remotely.

set -euo pipefail

# Argument from monero-wallet-rpc
TXID="$1"

# SSH connection details
WEB_USER="mnp-web"
WEB_HOST="web.example.com"
SSH_KEY="/var/lib/mnp_keys/mnp_notify_id_ed25519"

# Path to mnp binary on the webserver (adjust if necessary)
MNP_BIN="/usr/local/bin/mnp"

# Local log file on the wallet host
LOGFILE="/var/log/mnp_notify.log"

# Command to execute remotely
REMOTE_CMD="$MNP_BIN --confirmation 5 $TXID"

# Run and log the result
{
  echo "[$(date --iso-8601=seconds)] Sending confirmation for TXID=$TXID to ${WEB_USER}@${WEB_HOST}"
  ssh -i "$SSH_KEY" -o BatchMode=yes -o StrictHostKeyChecking=yes \
      "${WEB_USER}@${WEB_HOST}" "$REMOTE_CMD"
  RC=$?
  echo "[$(date --iso-8601=seconds)] Result code: $RC"
} >> "$LOGFILE" 2>&1

Usage with monero-wallet-rpc

In your monero-wallet-rpc.cfg, add the following line (see also the config-file section in your wiki):

notify-at=/usr/local/bin/notify_mnp.sh %s

Whenever monero-wallet-rpc triggers a notify event, this script will run and forward the TXID confirmation to the webserver.

Security best practices 1 (Web Host)

The SSH key mnp_notify_id_ed25519 should be a dedicated key. On the webserver, restrict it in ~/.ssh/authorized_keys with a forced command, e.g.:

command="/usr/local/bin/mnp-wrapper.sh",no-pty,no-agent-forwarding,no-X11-forwarding ssh-ed25519 AAAA... notify-key

→ This ensures that the key can only be used to run your wrapper script (and not arbitrary commands).

The mnp-web user on the webserver should have no extra privileges.

The local logfile /var/log/mnp_notify.log on the wallet host helps with debugging.

secure mnp-wrapper.sh implementation

You can drop onto your webserver. This script is meant to be used as a forced command in authorized_keys, so the dedicated SSH key can only trigger mnp --confirmation .

#!/usr/bin/env bash
#
# mnp-wrapper.sh
#
# Forced-command wrapper for mnp confirmation requests.
# This is called automatically by sshd when the notify SSH key is used.
#
# It validates the input TXID and executes `mnp --confirmation <TXID>` safely.

set -euo pipefail

LOGFILE="/var/log/mnp_wrapper.log"
MNP_BIN="/usr/local/bin/mnp"

# Read all arguments passed by sshd (forced command may still receive them)
TXID="${SSH_ORIGINAL_COMMAND##* }"  # extract last token if present

# If no TXID is passed via SSH_ORIGINAL_COMMAND, try stdin (optional)
if [[ -z "$TXID" ]] && [[ ! -t 0 ]]; then
  read -r TXID
fi

# Validate TXID format (64 hex chars)
if [[ ! "$TXID" =~ ^[0-9a-fA-F]{64}$ ]]; then
  echo "Invalid TXID: $TXID" >&2
  echo "[$(date --iso-8601=seconds)] Invalid TXID attempt: $TXID from $SSH_CONNECTION" >> "$LOGFILE"
  exit 1
fi

{
  echo "[$(date --iso-8601=seconds)] Received TXID=$TXID from $SSH_CONNECTION"
  echo "Executing: $MNP_BIN --confirmation $TXID"
} >> "$LOGFILE"

# Run mnp --confirmation securely
exec "$MNP_BIN" --confirmation 5 "$TXID"

Setting up authorized_keys

In ~mnp-web/.ssh/authorized_keys on your webserver, use a forced command entry pointing to this wrapper:

command="/usr/local/bin/mnp-wrapper.sh",no-pty,no-agent-forwarding,no-X11-forwarding ssh-ed25519 AAAA... notify-key

This ensures:

When the notify SSH key is used, it always runs this wrapper script.

No interactive shell is granted.

The key cannot be used to run arbitrary commands.

sshfs

Why sshfs does not work for named pipes between hosts?

A FIFO (named pipe) is a kernel object that provides inter-process communication (IPC) only within a single operating system kernel.

When you create a FIFO with mkfifo on one machine, processes on that same machine can open the pipe for reading/writing, and the kernel synchronizes the data flow. But if you mount a remote directory with sshfs, what you see on the client is just the directory entry for the FIFO. The pipe semantics are not transferred across the network. On the client side, the kernel does not know about the remote kernel’s FIFO; it only sees an empty placeholder file.

So: the FIFO mechanism is purely local to each host’s kernel.

Man in the Middle Attack

Sign a payment

mnp-payment --subaddr 23 --amount 3334566 | gpg --clearsign > signedpayment.txt
mnp-payment --subaddr 23 --amount 3334566 | gpg --clearsign | mutt -s "Monero Payment Request" recipient@example.com

Verify a payment

cat signedpayment.txt | gpg --verify

Clone this wiki locally