command-line options for server: daemon, ssl certificates, tor and others (#70)

* --daemon and --stop options for server

* --restart and --force option

* ssl certificates and tor

* tor key path fix...

* pass host to flask

* fix bug with switching between nodes

* docs on node setup - reverse proxy and self-signed certs

* typo
This commit is contained in:
Stepan Snigirev 2020-02-25 20:57:36 +01:00 committed by GitHub
parent 556ec5f961
commit 9039bc2c4d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 358 additions and 35 deletions

2
.gitignore vendored
View file

@ -7,3 +7,5 @@ build
dist
*.egg-info
.vscode
cert.pem
key.pem

View file

@ -24,7 +24,7 @@ HWI support requires `libusb` (necessary? Or is `pip install libusb1` sufficient
* Ubuntu/Debian: `sudo apt install libusb-1.0-0-dev libudev-dev`
* macOS: `brew install libusb`
```
```sh
git clone https://github.com/cryptoadvance/specter-desktop.git
cd specter-desktop
virtualenv --python=python3 .env
@ -35,15 +35,29 @@ pip3 install -e .
Run the server:
```
```sh
cd specter-desktop
python3 -m cryptoadvance.specter server
```
You can also run it as a daemon, using tor, provide ssl certificates to run over https. Https is especially important because browsers don't allow the website to access camera without secure connection, and we need camera access to scan QR codes.
An example how to run specter server in the background (`--daemon`) with ssl certificates (`--key`, `--cert`) over tor:
```sh
python -m cryptoadvance.specter server --tor=mytorpassword --cert=./cert.pem --key=./key.pem --daemon
```
If your Bitcoin Core is using a default data folder the app should detect it automatically. If not, consider setting `rpcuser` and `rpcpassword` in the `bitcoin.conf` file and in the app settings.
Have a look at [DEVELOPMENT.md](https://github.com/cryptoadvance/specter-desktop/blob/master/DEVELOPMENT.md) for further information about hacking on specter-desktop.
## Detailed instructions
- Beyond local network - how to forward your node through a cheap VPS: [docs/reverse-proxy.md](docs/reverse-proxy.md)
- Setting up Specter over Tor: [docs/tor.md](docs/tor.md)
- Using self-signed certificates in local network or Tor: [docs/self-signed-certificates.md](docs/self-signed-certificates.md)
## A few screenshots
### Adding a new device
@ -69,7 +83,3 @@ Have a look at [DEVELOPMENT.md](https://github.com/cryptoadvance/specter-desktop
### Configuration
![](screenshots/bitcoin-rpc.jpg)
## Make Specter Desktop available externally over Tor
see: [docs/tor.md](docs/tor.md)

118
docs/reverse-proxy.md Normal file
View file

@ -0,0 +1,118 @@
# Specter over SSH-tunnel
If you want to have access to your wallet outside of your local network you can either [use Tor](./tor.md), or make a reverse proxy from your node to a cheap VPS somewhere.
Here we will describe how to set up your VPS server to forward all requests to your Bitcoin node.
You can either have both Specter and Bitcoin Core on the same node and forward Specter interface to remote server, or you can only do it for Bitcoin Core and keep Specter on your laptop. I will assume first option, if you want to go with the second one just change the port from `25441` to `8334` or whatever port your Core is using.
## Basic configuration
Update your remote server:
```sh
apt update && apt upgrade
```
Install nginx:
```sh
apt install nginx
```
Add a new server to your nginx configuration `/etc/nginx/sites-enabled/default`:
```sh
server {
listen 80 default_server;
listen [::]:80 default_server;
# optinaly configure domain name
server_name specter.mydomain.com;
# set proxy pass
location / {
proxy_pass http://127.0.0.1:25441;
}
}
```
Restart nginx:
```sh
nginx -s reload
```
On your local computer (where Specter is running) start a reverse proxy:
```sh
ssh -nN -R 25441:localhost:25441 user@specter.mydomain.com
```
Check in your browser - when you navigate to `http://specter.mydomain.com` you should see Specter already.
## Adding HTTPS
HTTPS is very important, not only because it is secure, but also because without HTTPS we can't use camera to scan QR codes. We need to get secure connection.
Letsencrypt issues certificates for free, they just need to check that you control the domain. So it doesn't work for local network or for Tor addresses. In these cases you would need to create a [self-signed certificate](./self-signed-certificates.md). But domains are cheap and probably everyone has a dozen that is not used. So we assume you have one.
Install certbot to issue certificate for us:
```sh
apt install software-properties-common
add-apt-repository universe
add-apt-repository ppa:certbot/certbot
apt update
apt install certbot python-certbot-nginx
```
Run `certbot` and answer it's questions:
```sh
certbot --nginx
```
Now Specter should be available over HTTPS: `https://specter.mydomain.com`
## Adding basic authentication
*Note:* This part is only required if you are running Specter on your node. If you are exposing your Bitcoin RPC it already has authentication with rpcuser and rpcpassword.
We don't want random people to have access to our wallet, so we want to protect it with login and password.
Nginx has a nice [documentation](https://docs.nginx.com/nginx/admin-guide/security-controls/configuring-http-basic-authentication/) on the topic, but I will copy-paste main commands here.
Verify that `apache2-utils` (Debian, Ubuntu) or `httpd-tools` (RHEL/CentOS/Oracle Linux) is installed.
```sh
apt install apache2-utils
```
Create your user (let's call it `specter`) and type the password:
```sh
htpasswd -c /etc/nginx/.htpasswd specter
```
Add two lines to the server block in the nginx config (`/etc/nginx/sites-enabled/default`):
```sh
server {
# ...
auth_basic "You shall not pass!";
auth_basic_user_file /etc/nginx/.htpasswd;
location / {
proxy_pass http://127.0.0.1:25441;
}
# ...
}
```
Restart nginx:
```sh
nginx -s reload
```
Now when you try to access the server it will ask for credentials.

View file

@ -0,0 +1,72 @@
# Issuing self-signed certificates
Browsers require secure communication with the server to use camera API. Without it we can't use QR code scanning.
If you are running a VPS it's easy - you just [issue a new certificate](./reverse-proxy#adding-https) with Letsencrypt.
If you are only using the node at home and want to use it from your local network you need to issue a certificate yourself.
On your node run this command:
```sh
openssl req -x509 -newkey rsa:4096 -nodes -out cert.pem -keyout key.pem -days 365
```
It will create two files - `cert.pem` and `key.pem`.
## Bare Specter over HTTPS
Provide these files to Specter as arguments:
```sh
python -m cryptoadvance.specter server --cert=./cert.pem --key=./key.pem
```
*Note:* Adding `--tor=your-tor-password` will create a tor hidden service with https.
## Specter with Nginx
Assuming you copied the files to `/etc/ssl/certs` and `/etc/ssl/private` add the following lines to server config (`/etc/nginx/sites-enabled/default`):
```sh
listen 443 ssl http2;
ssl_certificate /etc/ssl/certs/cert.pem;
ssl_certificate_key /etc/ssl/private/key.pem;
ssl_protocols TLSv1.2 TLSv1.1 TLSv1;
```
My config looks like this:
```
server{
listen 80 default_server;
listen 443 ssl http2;
server_name your_domain_or_ip;
ssl_certificate /etc/ssl/certs/cert.pem;
ssl_certificate_key /etc/ssl/private/key.pem;
ssl_protocols TLSv1.2 TLSv1.1 TLSv1;
location / {
proxy_pass http://127.0.0.1:25441;
}
}
```
## Adding certificate to trusted
With these certificates you should be able to navigate to your node using https, but you will see a scary warning.
In Firefox you can still proceed to the website, in Chrome you can't unless you add `cert.pem` file to trusted (on the phone you still can).
On **Mac**: copy `cert.pem` to your computer, add it to your keychain and set `Trust`:
- Don't forget to quit Chrome
- Start the Keychain Access app and open the `Certificates` category
- Drag your certificate file onto the Keychain Access window
- Right-click on your certificate and unfold the `Trust` list
- In row `When using this certificate`, choose `Always Trust`
Other platforms: ???

View file

@ -1,4 +1,5 @@
## Running Specter Desktop over a Tor hidden service
Specter Desktop protects your security and privacy by running on a local server that only talks to your own bitcoin node. But what if you need to check a wallet balance or generate and sign transactions when you're away from your home network?
Configuring your router to let you VPN into your home network is probably the easiest solution.
@ -14,15 +15,15 @@ Install Tor on the same server that you'll be running Specter Desktop:
* [macOS](https://2019.www.torproject.org/docs/tor-doc-osx.html.en)
### Configure Tor authentication
```
```sh
$ tor --hash-password "your-tor-passphrase"
```
That returns a password hash such as:
```
```sh
16:CE9058DA89498A4160373C70FF7FFF70CC2E20B6788FC48F5C35B2E85B
```
Update your `torrc` config file (usually `/etc/tor/torrc` or `/usr/local/etc/tor/torrc` on macOS Homebrew installs). Uncomment the `ControlPort` line as well as the `HashedControlPassword` line. Remember to paste in your own hashed password result from above.
```
```sh
## The port on which Tor will listen for local connections from Tor
## controller applications, as documented in control-spec.txt.
ControlPort 9051
@ -36,10 +37,18 @@ Restart the Tor service:
* `sudo /etc/init.d/tor restart` on linux
* `brew services restart tor` on macOS Homebrew installs
### Running with Tor using command line
### Configure Specter Desktop to connect to Tor
Update the `.flaskenv` file in the project root. Set `CONNECT_TOR` to 'True' and set `FLASK_ENV` to 'production':
You can start the server and provide your tor password using `--tor=` flag:
```sh
$ python3 -m cryptoadvance.specter server --tor=your-tor-passphrase
```
### Configure environment variables
Update the `.flaskenv` file in the project root. Set `CONNECT_TOR` to 'True' and set `FLASK_ENV` to 'production':
```sh
PORT=25441
# If you want to serve over a Tor hidden service, also set FLASK_ENV=production.
@ -51,24 +60,27 @@ FLASK_ENV=production
```
### Specify Tor secrets
The Tor password that we hashed above will need to be shared with Specter Desktop.
Copy the example `.env_example` file:
```
```sh
$ cp .env_example .env
```
And then edit `.env` and specify `TOR_PASSWORD`:
```
```sh
# The cleartext password that was entered into:
# $ tor --hash-password "your-tor-passphrase"
TOR_PASSWORD=your-tor-passphrase
```
### Launch with Tor
Now just start Specter Desktop as usual:
```
$ python server.py
```sh
$ python3 -m cryptoadvance.specter server
```
Amongst the startup output you'll see:

View file

@ -25,6 +25,8 @@ urllib3==1.25.6
Werkzeug==0.16.0
serial==0.0.97
pyserial==3.4
daemonize==2.5.0
pyOpenSSL==19.1.0
# only for testing currently
docker==4.1.0
pytest==5.2.2

View file

@ -13,6 +13,10 @@ from .bitcoind import (BitcoindDockerController,
from .helpers import load_jsons, which
from .server import DATA_FOLDER, create_app
from daemonize import Daemonize
from os import path
import signal
DEBUG = True
@click.group()
@ -21,7 +25,43 @@ def cli():
@cli.command()
def server():
@click.option("--daemon", is_flag=True)
@click.option("--stop", is_flag=True)
@click.option("--restart", is_flag=True)
@click.option("--force", is_flag=True)
# options below can help to run it on a remote server,
# but better use nginx
@click.option("--port") # default - 25441 set to 80 for http, 443 for https
@click.option("--host", default="127.0.0.1") # set to 0.0.0.0 to make it available outside
# for https:
@click.option("--cert")
@click.option("--key")
# provide tor password here
@click.option("--tor")
def server(daemon, stop, restart, force, port, host, cert, key, tor):
# we will store our daemon PIN here
pid_file = path.expanduser(path.join(DATA_FOLDER, "daemon.pid"))
toraddr_file = path.expanduser(path.join(DATA_FOLDER, "onion.txt"))
# check if pid file exists
if path.isfile(pid_file):
# if we need to stop daemon
if stop or restart:
print("Stopping the Specter server...")
with open(pid_file) as f:
pid = int(f.read())
os.kill(pid, signal.SIGTERM)
elif daemon:
if not force:
print(f"PID file \"{pid_file}\" already exists. Use --force to overwrite")
return
if stop:
return
else:
if stop or restart:
print(f"Can't find PID file \"{pid_file}\"")
if stop:
return
app = create_app()
# watch templates folder to reload when something changes
extra_dirs = ['templates']
@ -33,15 +73,67 @@ def server():
if os.path.isfile(filename):
extra_files.append(filename)
# Note: dotenv doesn't convert bools!
if os.getenv('CONNECT_TOR', 'False') == 'True' and os.getenv('TOR_PASSWORD') is not None:
import tor_util
tor_util.run_on_hidden_service(
app, port=os.getenv('PORT'),
debug=DEBUG, extra_files=extra_files
)
# if port is not defined - get it from environment
if port is None:
port = int(os.getenv('PORT', 25441))
else:
app.run(port=os.getenv('PORT'), debug=DEBUG, extra_files=extra_files)
port = int(port)
# certificates
if cert is None:
cert = os.getenv('CERT', None)
if key is None:
key = os.getenv('KEY', None)
protocol = "http"
kwargs = {
"host": host,
"port": port,
"extra_files": extra_files,
}
if cert is not None and key is not None:
cert = os.path.abspath(cert)
key = os.path.abspath(key)
kwargs["ssl_context"] = (cert, key)
protocol = "https"
# if tor password is not provided but env variable is set
if tor is None and os.getenv('CONNECT_TOR') == 'True':
from dotenv import load_dotenv
load_dotenv() # Load the secrets from .env
tor = os.getenv('TOR_PASSWORD')
def run(debug=False):
# Note: dotenv doesn't convert bools!
if tor is not None:
from . import tor_util
# if we have certificates
if "ssl_context" in kwargs:
tor_port = 443
else:
tor_port = 80
tor_util.run_on_hidden_service(app,
debug=False,
tor_password=tor,
tor_port=tor_port,
save_address_to=toraddr_file,
**kwargs)
else:
app.run(debug=debug, **kwargs)
# check if we should run a daemon or not
if daemon or restart:
print("Starting server in background...")
print("* Hopefully running on %s://%s:%d/" % (protocol, host, port))
if tor is not None:
print("* For onion address check the file %s" % toraddr_file)
# Note: we can't run flask as a deamon in debug mode,
# so use debug=False by default
d = Daemonize(app="specter", pid=pid_file, action=run)
d.start()
# if not a daemon we can use DEBUG
else:
run(DEBUG)
@cli.command()
@click.option('--debug/--no-debug', default=False)

View file

@ -332,6 +332,13 @@ class WalletManager:
self.cli = cli
if self.working_folder is not None:
self._wallets = load_jsons(self.working_folder, key="name")
try:
existing_wallets = [w["name"] for w in self.cli.listwalletdir()["wallets"]]
for k in self._wallets:
if self.path+k not in existing_wallets:
self._wallets.pop(w["name"])
except:
pass
else:
self._wallets = {}

View file

@ -1,23 +1,26 @@
import os
import stem
from stem.control import Controller
from .server import DATA_FOLDER
from dotenv import load_dotenv
load_dotenv() # Load the secrets from .env
def run_on_hidden_service(app, tor_password=None, tor_port=80, save_address_to=None, **kwargs):
port = 5000 # default flask port
if "port" in kwargs:
port = kwargs["port"]
else:
kwargs["port"] = port
def run_on_hidden_service(app, port, debug, extra_files):
with Controller.from_port() as controller:
print(' * Connecting to tor')
controller.authenticate(os.getenv('TOR_PASSWORD'))
controller.authenticate(tor_password)
key_path = os.path.expanduser('.tor_service_key')
key_path = os.path.abspath(os.path.expanduser(os.path.join(DATA_FOLDER,'.tor_service_key')))
tor_service_id = None
if not os.path.exists(key_path):
service = controller.create_ephemeral_hidden_service({80: port}, await_publication = True)
service = controller.create_ephemeral_hidden_service({tor_port: port}, await_publication = True)
tor_service_id = service.service_id
print("Started a new hidden service with the address of %s.onion" % tor_service_id)
print("* Started a new hidden service with the address of %s.onion" % tor_service_id)
with open(key_path, 'w') as key_file:
key_file.write('%s:%s' % (service.private_key_type, service.private_key))
@ -25,12 +28,17 @@ def run_on_hidden_service(app, port, debug, extra_files):
with open(key_path) as key_file:
key_type, key_content = key_file.read().split(':', 1)
service = controller.create_ephemeral_hidden_service({80: port}, key_type = key_type, key_content = key_content, await_publication = True)
service = controller.create_ephemeral_hidden_service({tor_port: port}, key_type = key_type, key_content = key_content, await_publication = True)
tor_service_id = service.service_id
print("Resumed %s.onion" % tor_service_id)
print("* Resumed %s.onion" % tor_service_id)
# save address to file
if save_address_to is not None:
with open(save_address_to, "w") as f:
f.write("%s.onion" % tor_service_id)
try:
app.run(port=port, debug=debug, extra_files=extra_files)
app.run(**kwargs)
finally:
if tor_service_id:
print(" * Shutting down our hidden service")