13 votes

Two-factor authentication for home VNC via Signal

For my particular use case I share my home PC with my spouse and since I'm the more tech-savvy of the two I'll need to occasionally remote in and help out with some random task. They know enough that the issue will usually be too complex to simply guide over the phone, so remote control it is.

I'm also trying to improve my personal efforts toward privacy and security. To that end I want to avoid closed-source services such as TeamViewer where a breach on their end could compromise my system.

The following is the current state of what I'm now using as I think others may benefit from this as well:

Setup

Web

I use a simple web form as my first authentication. It's just a username and password, but it does require a web host that supports server side code such as PHP. In my case I just created a blank page with nothing other than the form and when successful the page generates a 6 digit PIN and saves it to a text file in a private folder (so no one can simply navigate to it and get the PIN).

I went the text file route because my current hosting plan only allows 1 database and I didn't want to add yet another random table just for this 1 value.

Router

To connect to my home PC I needed to forward a port from my router. I'm going to use VNC as it lets me see what is currently shown on the monitor and work with someone already there so I forward port 5900 as VNC's default port. You can customize this if you want. Some routers allow you to SSH into their system and make changes that way so a step more secure would be to leave the port forward disabled and only enable it once a successful login from the web form is disabled. In my case I'll just leave the port forwarded all the time.

IP Address

To connect to my computer I need to know it's external IP address and for this I use FreeDNS from Afraid.org. My router has dynamic DNS support for them already included so it was easy to plug in my details to generate a URL which will always point to my home PC (well, as long as my router properly sends them my latest IP address). If your router doesn't support the dynamic DNS you choose many also allow either a download or the settings you would need to script your own to keep your IP address up to date with their service.

Signal

Signal is an end-to-end encrypted messenger which supports text, media, phone and video calls. There's also a nifty command line option on Github called Signal-cli which I'm using to provide my second form of authentication. I just downloaded the package, moved to my $PATH (in my case /usr/local/bin) and set it up as described on their README. In my case I have both a normal cell phone number and another number provided by Google Voice. I already use my normal cell phone number with Signal so for this project I used Signal-cli to register a new account using my Google Voice number.

VNC

My home PC runs Ubuntu 18.04 so I'm using x11vnc as my VNC server. Since I'm leaving my port forwarded all the time I most certainly do NOT want to leave VNC also running. That's too large a security risk for me. Instead I've written a short bash script that first checks the web form using curl and https (so it's encrypted) with its own login information to check if any PIN numbers have been saved. If a PIN is found the web server sends that back and then deletes the PIN text file. Meanwhile the bash script uses the PIN to start a VNC session with that PIN as the password and also sends my normal cell the PIN via Signal-cli so that I can login.

I have this script set to run every minute so I'm not waiting long after web login and I also have the x11vnc session set to timeout after a minute so I can quickly connect again should I mess something up. It's also important that x11vnc is set to auto exit after closing the session so that it's not left up for an attacker to attempt to abuse.

System Flow

Once everything is setup and working this is what it's like for me to connect to my home PC:

  1. Browse to my web form and login
  2. Close web form and wait for Signal message
  3. Launch VNC client
  4. Connect via dynamic DNS address (saved to VNC client)
  5. Enter PIN code
  6. Close VNC when done

Code

Here's some snippets to help get you started

PHP for Web Form Processing

<?php
// Variables
$username = 'your_username';
$password = 'your_password_super_long_and_unique';
$filename = 'path_to_private_folder/vnc/pin.txt';

// Process the login form
if($action == 'Login'){
	$file = fopen($filename,'w');
	$passwd = rand(100000,999999);
	fwrite($file,$passwd);
	fclose($file);
	exit('Success');
}

// Process the bash script
if($action == 'bash'){
	if(file_exists($filename)){
		$file = fopen($filename,'r');
		$passwd = fread($file,filesize($filename));
		fclose($filename);
		unlink($filename);
		exit($passwd);
	} else {
		exit('No_PIN');
	}
}
?>

Bash for x11vnc and Signal-cli

# See if x11vnc access has been requested
status=$(curl -s -d "u=your_username&p=your_password_super_long_and_unique&a=bash" https://vnc_web_form.com)

# Exit if nothing has been requested
if [ "$status" = "No_PIN" ]; then
  # No PIN so exit; log the event if you want
  exit 0
fi

# Strip non-numeric characters
num="${status//[!0-9]/}"

# See if they still match (prevent error messages from triggering stuff)
if [ $status != $num ]; then
  # They don't match so probably not a PIN - exit; log it if you want
  exit 1
fi

# Validate pin number
num=$((num + 0))
if [ $num -lt 100000 ]; then
  # PIN wasn't 6 digits so something weird is going on - exit; log it if you want
  exit 1
fi
if [ $num -gt 999999 ]; then
  # Same as before
  exit 1
fi

# Everything is good; start up x11vnc
# Log event if you want

# Get the current IP address - while dynamic DNS is in place this serves as a backup
ip=$(dig +short +timeout=5 myip.opendns.com @resolver1.opendns.com)

# Send IP and password via Signal
# Note that phone number includes country code
# My bash is running as root so I run the command as my local user where I had registered Signal-cli
su -c "signal-cli -u +google_voice_number send -m '$num for $ip' +normal_cell_number" s3rvant

# Status was requested and variable is now the password
# this provides a 1 minute window to connect with 1-time password to control main display
# again run as local user
su -c "x11vnc -timeout 60 -display :0 -passwd $num" s3rvant

Final Thoughts

There are more secure ways to handle this. Some routers support VPN for the connect along with device certificates which are much stronger than a 6 digit PIN code. Dynamically opening and closing the router port as part of the bash script would also be a nice touch. For me this is enough security and is plenty convenient enough to quickly offer tech support (or nab some bash code for articles like this) on the fly.

I'm pretty happy with how Signal-cli has worked out and plan to use it again with my next project (home automation). I'll be sure to post again once I get that ball rolling.

11 comments

  1. [2]
    Comment deleted by author
    Link
    1. s3rvant
      Link Parent
      Wireguard sounds pretty great at first glance. Will read through it more thoroughly later tonight. Thanks much for letting me know about it!

      Wireguard sounds pretty great at first glance. Will read through it more thoroughly later tonight. Thanks much for letting me know about it!

      2 votes
  2. [2]
    zmaile
    (edited )
    Link
    An attacker would be able to see if you've started the login process because VNC's open port will become connectable to anyone once the service starts. From there, one only has to guess the...

    An attacker would be able to see if you've started the login process because VNC's open port will become connectable to anyone once the service starts.

    From there, one only has to guess the username, and try the 100k 900k possible password combinations. If this was a targeted attack, then they probably know some likely usernames to try too. So now the only thing stopping them from connecting is them being able to try all 100k passwords before you log in.

    So my first advice would be to set up fail2ban for the VNC service, and use a better password than a 6 digit number. But better again would be a secure connection (VPN, SSH tunnel, whatever) that requires a key (e.g. openssl can be used). Of course, then you need a way to carry the key around with you in a secure way.

    2 votes
    1. s3rvant
      Link Parent
      Yes, I agree my current solution is not the most secure; it is simply the best 2FA I’ve been able to come up with so far. I’ve started researching my options for VPN as I think that would work...

      Yes, I agree my current solution is not the most secure; it is simply the best 2FA I’ve been able to come up with so far.

      I’ve started researching my options for VPN as I think that would work best for my use case while also supporting 2FA on each of the devices I work with.

  3. [8]
    s3rvant
    Link
    Hmm, not sure why the PHP syntax highlighting didn't work; if anyone knows I'll edit to fix

    Hmm, not sure why the PHP syntax highlighting didn't work; if anyone knows I'll edit to fix

    3 votes
    1. [2]
      Deimos
      Link Parent
      Hmm, from looking at the Pygments documentation about it, it looks like by default it will only highlight if you include the <?php at the start.

      Hmm, from looking at the Pygments documentation about it, it looks like by default it will only highlight if you include the <?php at the start.

      3 votes
      1. s3rvant
        Link Parent
        Thanks; that did the trick

        Thanks; that did the trick

        1 vote
    2. [5]
      Soptik
      Link Parent
      It looks like you have to wrap it in <?php ?>. Here's example. I'll try to find a way how to fix it, I'm sure this isn't a feature.

      It looks like you have to wrap it in <?php ?>. Here's example. I'll try to find a way how to fix it, I'm sure this isn't a feature.

      3 votes
      1. [3]
        Deimos
        Link Parent
        We'd need to check if the selected lexer is PhpLexer and pass startinline=True as an extra argument.

        We'd need to check if the selected lexer is PhpLexer and pass startinline=True as an extra argument.

        4 votes
        1. [2]
          Soptik
          Link Parent
          I can implement it and create merge request if you don't mind. Hopefully it'll be working on first try, not like last time :-)

          I can implement it and create merge request if you don't mind. Hopefully it'll be working on first try, not like last time :-)

          4 votes
          1. Deimos
            Link Parent
            Sure, that would be good, thanks.

            Sure, that would be good, thanks.

            2 votes
      2. s3rvant
        Link Parent
        Thanks; that did the trick

        Thanks; that did the trick

        1 vote