Update HaProxy document, move server/admin to correct place

This commit is contained in:
Griatch 2021-05-25 15:13:57 +02:00
parent d4f61f1a14
commit 59dd0b007a
9 changed files with 266 additions and 83 deletions

View file

@ -1,62 +1,243 @@
# HAProxy Config (Optional)
## Making Evennia, HTTPS and WSS (Secure Websockets) play nicely together
### Evennia, HTTPS and Secure Websockets can play nicely together, quickly.
A modern public-facing website should these days be served via encrypted
connections. So `https:` rather than `http:` for the website and
`wss:` rather than vs `ws:` for websocket connections used by webclient.
This sets up HAProxy 1.5+ in front of Evennia to provide security.
The reason is security - not only does it make sure a user ends up at the right
site (rather than a spoof that hijacked the original's address), it stops an
evil middleman from snooping on data (like passwords) being sent across the
wire.
Evennia itself does not implement https/wss connections. This is something best
handled by dedicated tools able to keep up-to-date with the latest security
practices.
So what we'll do is install _proxy_ between Evennia and the outgoing ports of
your server. Essentially, Evennia will think it's only running locally (on
localhost, IP 127.0.0.1) while the proxy will transparently map that to the
"real" outgoing ports and handle HTTPS/WSS for us.
Evennia
|
(inside-only local IP/ports serving HTTP/WS)
|
Proxy
|
(outside-visible public IP/ports serving HTTPS/WSS)
|
Firewall
|
Internet
These instructions assume you run a server with Unix/Linux (very common if you
use remote hosting) and that you have root access to that server.
The pieces we'll need:
- [HAProxy](https://www.haproxy.org/) - an open-source proxy program that is
easy to set up and use.
- [LetsEncrypt](https://letsencrypt.org/getting-started/) for providing the User
Certificate needed to establish an encrypted connection. In particular we'll
use the excellent [Certbot](https://certbot.eff.org/instructions) program,
which automates the whole certificate setup process with LetsEncrypt.
- `cron` - this comes with all Linux/Unix systems and allows to automate tasks
in the OS.
Before starting you also need the following information and setup:
- (optional) The host name of your game. This is
something you must previously have purchased from a _domain registrar_ and set
up with DNS to point to the IP of your server. For the benefit of this
manual, we'll assume your host name is `my.awesomegame.com`.
- If you don't have a domain name or haven't set it up yet, you must at least
know the IP address of your server. Find this with `ifconfig` or similar from
inside the server. If you use a hosting service like DigitalOcean you can also
find the droplet's IP address in the control panel. Use this as the host name
everywhere.
- You must open port 80 in your firewall. This is used by Certbot below to
auto-renew certificates. So you can't really run another webserver alongside
this setup without tweaking.
- You must open port 443 (HTTPS) in your firewall. This will be the external
webserver port.
- Make sure port 4001 (internal webserver port) is _not_ open in your firewall
(it usually will be closed by default unless you explicitly opened it
previously).
- Open port 4002 in firewall (we'll use the same number for both internal-
and external ports, the proxy will only show the safe one serving wss).
## Getting certificates
Certificates guarantee that you are you. Easiest is to get this with
[Letsencrypt](https://letsencrypt.org/getting-started/) and the
[Certbot](https://certbot.eff.org/instructions) program. Certbot has a lot of
install instructions for various operating systems. Here's for Debian/Ubuntu:
sudo apt install certbot
Make sure to stop Evennia and that no port-80 using service is running, then
sudo certbot certonly --standalone
You will get some questions you need to answer, such as an email to send
certificate errors to and the host name (or IP, supposedly) to use with this
certificate. After this, the certificates will end up in
`/etc/letsencrypt/live/<yourhostname>/*pem` (example from Ubuntu). The
critical files for our purposes are `fullchain.pem` and `privkey.pem`.
Certbot sets up a cron-job/systemd job to regularly renew the certificate. To
check this works, try
Installing HAProxy is usually as simple as:
```
# Redhat derivatives
yum install haproxy
# dnf instead of yum for very recent Fedora distros.
```
or
```
# Debian derivatives
apt install haproxy
sudo certbot renew --dry-run
```
Configuration of HAProxy requires a single file given as an argument on the command line:
```
haproxy -f /path/to/config.file
```
The certificate is only valid for 3 months at a time, so make sure this test
works (it requires port 80 to be open). Look up Certbot's page for more help.
In it (example using haproxy 1.5.18 on Centos7):
```
# stuff provided by the default haproxy installs
We are not quite done. HAProxy expects these two files to be _one_ file. More
specifically we are going to
1. copy `privkey.pem` and copy it to a new file named `<yourhostname>.pem` (like
`my.awesomegame.com.pem`)
2. Append the contents of `fullchain.pem` to the end of this new file. No empty
lines are needed.
We could do this by copy&pasting in a text editor, but here's how to do it with
shell commands (replace the example paths with your own):
cd /etc/letsencrypt/live/my.awesomegame.com/
sudo cp privkey.pem my.awesomegame.com.pem
sudo cat fullchain.pem >> my.awesomegame.com.pem
The new `my.awesomegame.com.pem` file (or whatever you named it) is what we will
point to in the HAProxy config below.
There is a problem here though - Certbot will (re)generate `fullchain.pem` for
us automatically a few days before before the 3-month certificate runs out.
But HAProxy will not see this because it is looking at the combined file that
will still have the old `fullchain.pem` appended to it.
We'll set up an automated task to rebuild the `.pem` file regularly by
using the `cron` program of Unix/Linux.
crontab -e
An editor will open to the crontab file. Add the following at the bottom (all
on one line, and change the paths to your own!):
0 5 * * * cd /etc/letsencrypt/live/my.awesomegame.com/ &&
cp privkey.pem my.awesomegame.com.pem &&
cat fullchain.pem >> my.awesomegame.com.pem
Save and close the editor. Every night at 05:00 (5 AM), the
`my.awesomegame.com.pem` will now be rebuilt for you. Since Certbot updates
the `fullchain.pem` file a few days before the certificate runs out, this should
be enough time to make sure HaProxy never sees an outdated certificate.
## Installing and configuring HAProxy
Installing HaProxy is usually as simple as:
# Debian derivatives (Ubuntu, Mint etc)
sudo apt install haproxy
# Redhat derivatives (dnf instead of yum for very recent Fedora distros)
sudo yum install haproxy
Configuration of HAProxy is done in a single file. This can be located wherever
you like, for now put in your game dir and name it `haproxy.cfg`.
Here is an example tested on Centos7 and Ubuntu. Make sure to change the file to
put in your own values.
We use the `my.awesomegame.com` example here and here are the ports
- `443` is the standard SSL port
- `4001` is the standard Evennia webserver port (firewall closed!)
- `4002` is the default Evennia websocket port (we use the same number for
the outgoing wss port, so this should be open in firewall).
```shell
# base stuff to set up haproxy
global
log /dev/log local0
chroot /var/lib/haproxy
maxconn 4000
user haproxy
tune.ssl.default-dh-param 2048
## uncomment this when everything works
# daemon
defaults
mode http
option forwardfor
# Evennia Specifics
listen evennia-https-website
bind <public-ip-address>:<public-SSL-port--probably-443> ssl no-sslv3 no-tlsv10 crt
/path/to/your-cert.pem
server localhost 127.0.0.1:<evennia-web-port-probably-4001>
listen evennia-secure-websocket
bind <public-ip-address>:<WEBSOCKET_CLIENT_URL 4002> ssl no-sslv3 no-tlsv10 crt /path/to/your-
cert.pem
server localhost 127.0.0.1:<WEBSOCKET_CLIENT_URL 4002>
bind my.awesomegame.com:443 ssl no-sslv3 no-tlsv10 crt /etc/letsencrypt/live/my.awesomegame.com>/my.awesomegame.com.pem
server localhost 127.0.0.1:4001
timeout client 10m
timeout server 10m
timeout connect 5m
listen evennia-secure-websocket
bind my.awesomegame.com:4002 ssl no-sslv3 no-tlsv10 crt /etc/letsencrypt/live/my.awesomegame.com/my.awesomegame.com.pem
server localhost 127.0.0.1:4002
timeout client 10m
timeout server 10m
timeout connect 5m
```
Then edit mygame/server/conf/settings.py and add:
```
WEBSERVER_INTERFACES = ['127.0.0.1']
WEBSOCKET_CLIENT_INTERFACE = '127.0.0.1'
```
or
```
LOCKDOWN_MODE=True
```
## Putting it all together
Get back to the Evennia game dir and edit mygame/server/conf/settings.py. Add:
WEBSERVER_INTERFACES = ['127.0.0.1']
WEBSOCKET_CLIENT_INTERFACE = '127.0.0.1'
and
WEBSOCKET_CLIENT_URL="wss://my.awesomegame.com:4002/"
Make sure to reboot (stop + start) evennia completely:
evennia reboot
Finally you start the proxy:
```
WEBSOCKET_CLIENT_URL="wss://yourhost.com:4002/"
sudo haproxy -f /path/to/the/above/haproxy.cfg
```
Make sure you can connect to your game from your browser and that you end up
with an `https://` page and can use the websocket webclient.
Once everything works you may want to start the proxy automatically and in the
background. Stop the proxy with `Ctrl-C` and make sure to uncomment the line `#
daemon` in the config file.
If you have no other proxies running on your server, you can copy your
haproxy.conf file to the system-wide settings:
sudo cp /path/to/the/above/haproxy.cfg /etc/haproxy/
The proxy will now start on reload and you can control it with
sudo service haproxy start|stop|restart|status
If you don't want to copy stuff into `/etc/` you can also run the haproxy purely
out of your current location by running it with `cron` on server restart. Open
the crontab again:
sudo crontab -e
Add a new line to the end of the file:
@reboot haproxy -f /path/to/the/above/haproxy.cfg
Save the file and haproxy should start up automatically when you reboot the
server. Next just restart the proxy manually a last time - with `daemon`
uncommented in the config file, it will now start as a background process.

View file

@ -51,6 +51,7 @@ _MUDINFO_CHANNEL = None
_CONNECT_CHANNEL = None
_CMDHANDLER = None
# Create throttles for too many account-creations and login attempts
CREATION_THROTTLE = Throttle(
name='creation', limit=settings.CREATION_THROTTLE_LIMIT, timeout=settings.CREATION_THROTTLE_TIMEOUT

View file

@ -98,7 +98,7 @@ class HelpEntry(SharedMemoryModel):
def aliases(self):
return AliasHandler(self)
class Meta(object):
class Meta:
"Define Django meta options"
verbose_name = "Help Entry"
verbose_name_plural = "Help Entries"

View file

@ -8,9 +8,10 @@ Config values should usually be set through the
manager's conf() method.
"""
import pickle
from django.db import models
from django.urls import reverse
from django.contrib.contenttypes.models import ContentType
from evennia.utils.idmapper.models import WeakSharedMemoryModel
from evennia.utils import logger, utils
from evennia.utils.dbserialize import to_pickle, from_pickle
@ -110,7 +111,7 @@ class ServerConfig(WeakSharedMemoryModel):
value = property(__value_get, __value_set, __value_del)
class Meta(object):
class Meta:
"Define Django meta options"
verbose_name = "Server Config value"
verbose_name_plural = "Server Config values"
@ -118,9 +119,8 @@ class ServerConfig(WeakSharedMemoryModel):
#
# ServerConfig other methods
#
def __repr__(self):
return "<{} {}>".format(self.__class__.__name__, self.key, self.value)
return "<{} {}>".format(self.__class__.__name__, self.key)
def store(self, key, value):
"""

View file

@ -13,7 +13,7 @@ class Throttle(object):
This version of the throttle is usable by both the terminal server as well
as the web server, imposes limits on memory consumption by using deques
with length limits instead of open-ended lists, and uses native Django
with length limits instead of open-ended lists, and uses native Django
caches for automatic key eviction and persistence configurability.
"""
@ -37,28 +37,28 @@ class Throttle(object):
except Exception as e:
logger.log_trace("Throttle: Errors encountered; using default cache.")
self.storage = caches['default']
self.name = kwargs.get('name', 'undefined-throttle')
self.limit = kwargs.get("limit", 5)
self.cache_size = kwargs.get('cache_size', self.limit)
self.timeout = kwargs.get("timeout", 5 * 60)
def get_cache_key(self, *args, **kwargs):
"""
Creates a 'prefixed' key containing arbitrary terms to prevent key
collisions in the same namespace.
"""
return '-'.join((self.name, *args))
def touch(self, key, *args, **kwargs):
"""
Refreshes the timeout on a given key and ensures it is recorded in the
key register.
Args:
key(str): Key of entry to renew.
"""
cache_key = self.get_cache_key(key)
if self.storage.touch(cache_key, self.timeout):
@ -86,11 +86,11 @@ class Throttle(object):
keys_key = self.get_cache_key('keys')
keys = self.storage.get_or_set(keys_key, set(), self.timeout)
data = self.storage.get_many((self.get_cache_key(x) for x in keys))
found_keys = set(data.keys())
if len(keys) != len(found_keys):
self.storage.set(keys_key, found_keys, self.timeout)
return data
def update(self, ip, failmsg="Exceeded threshold."):
@ -107,14 +107,14 @@ class Throttle(object):
"""
cache_key = self.get_cache_key(ip)
# Get current status
previously_throttled = self.check(ip)
# Get previous failures, if any
entries = self.storage.get(cache_key, [])
entries.append(time.time())
# Store updated record
self.storage.set(cache_key, deque(entries, maxlen=self.cache_size), self.timeout)
@ -124,54 +124,54 @@ class Throttle(object):
# If this makes it engage, log a single activation event
if not previously_throttled and currently_throttled:
logger.log_sec(f"Throttle Activated: {failmsg} (IP: {ip}, {self.limit} hits in {self.timeout} seconds.)")
self.record_ip(ip)
def remove(self, ip, *args, **kwargs):
"""
Clears data stored for an IP from the throttle.
Args:
ip(str): IP to clear.
"""
exists = self.get(ip)
if not exists: return False
cache_key = self.get_cache_key(ip)
self.storage.delete(cache_key)
self.unrecord_ip(ip)
# Return True if NOT exists
return ~bool(self.get(ip))
def record_ip(self, ip, *args, **kwargs):
"""
Tracks keys as they are added to the cache (since there is no way to
Tracks keys as they are added to the cache (since there is no way to
get a list of keys after-the-fact).
Args:
ip(str): IP being added to cache. This should be the original
IP, not the cache-prefixed key.
"""
keys_key = self.get_cache_key('keys')
keys = self.storage.get(keys_key, set())
keys.add(ip)
self.storage.set(keys_key, keys, self.timeout)
return True
def unrecord_ip(self, ip, *args, **kwargs):
"""
Forces removal of a key from the key registry.
Args:
ip(str): IP to remove from list of keys.
"""
keys_key = self.get_cache_key('keys')
keys = self.storage.get(keys_key, set())
try:
try:
keys.remove(ip)
self.storage.set(keys_key, keys, self.timeout)
return True
@ -194,7 +194,7 @@ class Throttle(object):
"""
now = time.time()
ip = str(ip)
cache_key = self.get_cache_key(ip)
# checking mode
@ -210,4 +210,4 @@ class Throttle(object):
self.remove(ip)
return False
else:
return False
return False

View file

@ -75,7 +75,7 @@ class Tag(models.Model):
db_index=True,
)
class Meta(object):
class Meta:
"Define Django meta options"
verbose_name = "Tag"
unique_together = (("db_key", "db_category", "db_tagtype", "db_model"),)

View file

@ -12,3 +12,4 @@ from .scripts import ScriptAdmin
from .comms import ChannelAdmin, MsgAdmin
from .help import HelpEntryAdmin
from .tags import TagAdmin
from .server import ServerConfigAdmin

View file

@ -16,7 +16,7 @@ class HelpTagInline(TagInline):
class HelpEntryForm(forms.ModelForm):
"Defines how to display the help entry"
class Meta(object):
class Meta:
model = HelpEntry
fields = "__all__"
@ -29,9 +29,11 @@ class HelpEntryForm(forms.ModelForm):
required=False,
widget=forms.Textarea(attrs={"cols": "100", "rows": "2"}),
help_text="Set lock to view:all() unless you want it to only show to certain users."
"<BR>Use the `edit:` limit if wanting to limit who can edit from in-game. By default it's only "
"limited to who can use the `sethelp` command (Builders).")
"<BR>Use the `edit:` limit if wanting to limit who can edit from in-game. By default it's "
"only limited to who can use the `sethelp` command (Builders).")
@admin.register(HelpEntry)
class HelpEntryAdmin(admin.ModelAdmin):
"Sets up the admin manaager for help entries"
inlines = [HelpTagInline]
@ -59,5 +61,3 @@ class HelpEntryAdmin(admin.ModelAdmin):
},
),
)
admin.site.register(HelpEntry, HelpEntryAdmin)

View file

@ -1,12 +1,15 @@
#
# This sets up how models are displayed
# in the web admin interface.
#
"""
This sets up how models are displayed
in the web admin interface.
"""
from django.contrib import admin
from evennia.server.models import ServerConfig
@admin.register(ServerConfig)
class ServerConfigAdmin(admin.ModelAdmin):
"""
Custom admin for server configs
@ -20,6 +23,3 @@ class ServerConfigAdmin(admin.ModelAdmin):
save_as = True
save_on_top = True
list_select_related = True
admin.site.register(ServerConfig, ServerConfigAdmin)