diff --git a/README.md b/README.md index b71c1f9e..ce9b68f7 100644 --- a/README.md +++ b/README.md @@ -34,17 +34,16 @@ Congratulations! You should now have a working setup. You'll be given the addres The Goals --------- -* Create a push-button "Email Appliance" for everyday users. -* Promote decentralization, innovation, and privacy on the web. +I am trying to: + +* Make deploying a good mail server easy. +* Promote [decentralization](http://redecentralize.org/), innovation, and privacy on the web. * Have automated, auditable, and [idempotent](http://sharknet.us/2014/02/01/automated-configuration-management-challenges-with-idempotency/) configuration. - -For more background, see [The Rationale](https://github.com/mail-in-a-box/mailinabox/wiki). - -What I am not trying to do: - * **Not** to be a mail server that the NSA cannot hack. * **Not** to be customizable by power users. +For more background, see [The Rationale](https://github.com/mail-in-a-box/mailinabox/wiki). + The Acknowledgements -------------------- diff --git a/conf/nginx.conf b/conf/nginx.conf index 8a120b78..ea7e40d6 100644 --- a/conf/nginx.conf +++ b/conf/nginx.conf @@ -28,6 +28,7 @@ server { rewrite ^/admin$ /admin/; location /admin/ { proxy_pass http://localhost:10222/; + proxy_set_header X-Forwarded-For $remote_addr; } # Roundcube Webmail configuration. diff --git a/management/daemon.py b/management/daemon.py index fc5b652a..aa0cf609 100755 --- a/management/daemon.py +++ b/management/daemon.py @@ -172,6 +172,30 @@ def dns_update(): except Exception as e: return (str(e), 500) +@app.route('/dns/set/', methods=['POST']) +@app.route('/dns/set//', methods=['POST']) +@app.route('/dns/set///', methods=['POST']) +@authorized_personnel_only +def dns_set_record(qname, rtype="A", value=None): + from dns_update import do_dns_update, set_custom_dns_record + try: + # Get the value from the URL, then the POST parameters, or if it is not set then + # use the remote IP address of the request --- makes dynamic DNS easy. To clear a + # value, '' must be explicitly passed. + print(request.environ) + if value is None: + value = request.form.get("value") + if value is None: + value = request.environ.get("HTTP_X_FORWARDED_FOR") # normally REMOTE_ADDR but we're behind nginx as a reverse proxy + if value == '': + # request deletion + value = None + if set_custom_dns_record(qname, rtype, value, env): + return do_dns_update(env) + return "OK" + except ValueError as e: + return (str(e), 400) + @app.route('/dns/dump') @authorized_personnel_only def dns_get_dump(): diff --git a/management/dns_update.py b/management/dns_update.py index 24e12e35..eaee654c 100755 --- a/management/dns_update.py +++ b/management/dns_update.py @@ -5,6 +5,7 @@ ######################################################################## import os, os.path, urllib.parse, datetime, re, hashlib +import ipaddress import rtyaml from mailconfig import get_mail_domains @@ -551,6 +552,79 @@ def write_opendkim_tables(zonefiles, env): ######################################################################## +def set_custom_dns_record(qname, rtype, value, env): + # validate + rtype = rtype.upper() + if value is not None: + if rtype in ("A", "AAAA"): + v = ipaddress.ip_address(value) + if rtype == "A" and not isinstance(v, ipaddress.IPv4Address): raise ValueError("That's an IPv6 address.") + if rtype == "AAAA" and not isinstance(v, ipaddress.IPv6Address): raise ValueError("That's an IPv4 address.") + elif rtype in ("CNAME", "TXT"): + # anything goes + pass + else: + raise ValueError("Unknown record type '%s'." % rtype) + + # load existing config + config = get_custom_dns_config(env) + + # update + if qname not in config: + if value is None: + # Is asking to delete a record that does not exist. + return False + elif rtype == "A": + # Add this record using the short form 'qname: value'. + config[qname] = value + else: + # Add this record. This is the qname's first record. + config[qname] = { rtype: value } + else: + if isinstance(config[qname], str): + # This is a short-form 'qname: value' implicit-A record. + if value is None and rtype != "A": + # Is asking to delete a record that doesn't exist. + return False + elif value is None and rtype == "A": + # Delete record. + del config[qname] + elif rtype == "A": + # Update, keeping short form. + if config[qname] == "value": + # No change. + return False + config[qname] = value + else: + # Expand short form so we can add a new record type. + config[qname] = { "A": config[qname], rtype: value } + else: + # This is the qname: { ... } (dict) format. + if value is None: + if rtype not in config[qname]: + # Is asking to delete a record that doesn't exist. + return False + else: + # Delete the record. If it's the last record, delete the domain. + del config[qname][rtype] + if len(config[qname]) == 0: + del config[qname] + else: + # Update the record. + if config[qname].get(rtype) == "value": + # No change. + return False + config[qname][rtype] = value + + # serialize & save + config_yaml = rtyaml.dump(config) + with open(os.path.join(env['STORAGE_ROOT'], 'dns/custom.yaml'), "w") as f: + f.write(config_yaml) + + return True + +######################################################################## + def justtestingdotemail(domain, records): # If the domain is a subdomain of justtesting.email, which we own, # automatically populate the zone where it is set up on dns4e.com. diff --git a/management/web_update.py b/management/web_update.py index 2afdaea1..5131d9c9 100644 --- a/management/web_update.py +++ b/management/web_update.py @@ -177,6 +177,7 @@ def ensure_ssl_certificate_exists(domain, ssl_key, ssl_certificate, csr_path, en "openssl", "req", "-new", "-key", ssl_key, "-out", csr_path, + "-sha256", "-subj", "/C=%s/ST=/L=/O=/CN=%s" % (env["CSR_COUNTRY"], domain)]) # And then make the certificate. diff --git a/setup/management.sh b/setup/management.sh index 0f6e4d0a..7ea1332b 100755 --- a/setup/management.sh +++ b/setup/management.sh @@ -8,7 +8,7 @@ hide_output pip3 install rtyaml # Create a backup directory and a random key for encrypting backups. mkdir -p $STORAGE_ROOT/backup if [ ! -f $STORAGE_ROOT/backup/secret_key.txt ]; then - openssl rand -base64 2048 > $STORAGE_ROOT/backup/secret_key.txt + $(umask 077; openssl rand -base64 2048 > $STORAGE_ROOT/backup/secret_key.txt) fi # Link the management server daemon into a well known location. diff --git a/setup/migrate.py b/setup/migrate.py index 87c915ab..d2ecff24 100755 --- a/setup/migrate.py +++ b/setup/migrate.py @@ -56,6 +56,10 @@ def migration_4(env): db = os.path.join(env["STORAGE_ROOT"], 'mail/users.sqlite') shell("check_call", ["sqlite3", db, "ALTER TABLE users ADD privileges TEXT NOT NULL DEFAULT ''"]) +def migration_5(env): + # The secret key for encrypting backups was world readable. Fix here. + os.chmod(os.path.join(env["STORAGE_ROOT"], 'backup/secret_key.txt'), 0o600) + def get_current_migration(): ver = 0 while True: diff --git a/setup/ssl.sh b/setup/ssl.sh index d440219f..5c2280c3 100755 --- a/setup/ssl.sh +++ b/setup/ssl.sh @@ -31,7 +31,7 @@ if [ ! -f $STORAGE_ROOT/ssl/ssl_cert_sign_req.csr ]; then # Generate a certificate signing request if one doesn't already exist. hide_output \ openssl req -new -key $STORAGE_ROOT/ssl/ssl_private_key.pem -out $STORAGE_ROOT/ssl/ssl_cert_sign_req.csr \ - -subj "/C=$CSR_COUNTRY/ST=/L=/O=/CN=$PRIMARY_HOSTNAME" + -sha256 -subj "/C=$CSR_COUNTRY/ST=/L=/O=/CN=$PRIMARY_HOSTNAME" fi if [ ! -f $STORAGE_ROOT/ssl/ssl_certificate.pem ]; then # Generate a SSL certificate by self-signing if a SSL certificate doesn't yet exist. diff --git a/setup/web.sh b/setup/web.sh index 9d00a76c..be89711b 100755 --- a/setup/web.sh +++ b/setup/web.sh @@ -48,7 +48,7 @@ done # Remove obsoleted scripts. # exchange-autodiscover is now handled by Z-Push. for f in exchange-autodiscover; do - rm /usr/local/bin/mailinabox-$f.php + rm -f /usr/local/bin/mailinabox-$f.php done # Make some space for users to customize their webfinger responses. diff --git a/tools/mail.py b/tools/mail.py index 439594bd..f85ef89c 100755 --- a/tools/mail.py +++ b/tools/mail.py @@ -67,6 +67,7 @@ elif sys.argv[1] == "user" and len(sys.argv) == 2: # Dump a list of users, one per line. Mark admins with an asterisk. users = mgmt("/mail/users?format=json", is_json=True) for user in users: + if user['status'] == 'inactive': continue print(user['email'], end='') if "admin" in user['privileges']: print("*", end='')