mirror of
https://github.com/evennia/evennia.git
synced 2026-03-16 21:06:30 +01:00
Update HaProxy document, move server/admin to correct place
This commit is contained in:
parent
d4f61f1a14
commit
59dd0b007a
9 changed files with 266 additions and 83 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"),)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue