mirror of
https://github.com/mail-in-a-box/mailinabox.git
synced 2026-03-13 17:17:23 +01:00
Compare commits
61 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3fbbf56986 | ||
|
|
362bc060f6 | ||
|
|
c8fef45362 | ||
|
|
8d19eade85 | ||
|
|
d53332b7cf | ||
|
|
392d33b902 | ||
|
|
4305a71916 | ||
|
|
a4d8e12fd7 | ||
|
|
3cb5e109a3 | ||
|
|
e4a4b47fac | ||
|
|
8a35905d2e | ||
|
|
dbf4729109 | ||
|
|
86b9ef496c | ||
|
|
6336cc6452 | ||
|
|
bc79319864 | ||
|
|
62e88cff54 | ||
|
|
6e6c993724 | ||
|
|
f8b4e3775d | ||
|
|
fad69f85fa | ||
|
|
0029811de2 | ||
|
|
fe9ed3f70d | ||
|
|
aedfe62bb0 | ||
|
|
c4f00626ef | ||
|
|
fdad83a1bb | ||
|
|
20e11bbab3 | ||
|
|
5bbe9f9a04 | ||
|
|
7a93d219ef | ||
|
|
808522d895 | ||
|
|
be9efe0273 | ||
|
|
766b98c4ad | ||
|
|
c422543fdd | ||
|
|
cf33be4596 | ||
|
|
161d096139 | ||
|
|
b32cb6229b | ||
|
|
3dd5fff110 | ||
|
|
398a66dd4a | ||
|
|
cfba97e104 | ||
|
|
bbf78716fd | ||
|
|
b9820641aa | ||
|
|
8c00556bab | ||
|
|
16d148a8a9 | ||
|
|
e8264e9b6a | ||
|
|
4f2b223070 | ||
|
|
05e128cafb | ||
|
|
59e9952a61 | ||
|
|
2f9fd09b2f | ||
|
|
ec20d657ba | ||
|
|
34ba279b0a | ||
|
|
c28065cc56 | ||
|
|
04960d0b98 | ||
|
|
280de022cb | ||
|
|
fa1cad7fb2 | ||
|
|
1926bfa1c5 | ||
|
|
2b351208e0 | ||
|
|
96b02e68ee | ||
|
|
82f4f8b2eb | ||
|
|
d703b0a2a1 | ||
|
|
2e3796c4f7 | ||
|
|
ac238b9d28 | ||
|
|
25e6fa53c2 | ||
|
|
593fd242bf |
36
CHANGELOG.md
36
CHANGELOG.md
@@ -1,6 +1,40 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
v0.15 (January 1, 2016)
|
||||
-----------------------
|
||||
|
||||
Mail:
|
||||
|
||||
* Updated Roundcube to version 1.1.3.
|
||||
* Auto-create aliases for abuse@, as required by RFC2142.
|
||||
* The DANE TLSA record is changed to use the certificate subject public key rather than the whole certificate, which means the record remains valid after certificate changes (so long as the private key remains the same, which it does for us).
|
||||
|
||||
Control panel:
|
||||
|
||||
* When IPv6 is enabled, check that system services are accessible over IPv6 too, that the box's hostname resolves over IPv6, and that reverse DNS is setup correctly for IPv6.
|
||||
* Explanatory text for setting up secondary nameserver is added/fixed.
|
||||
* DNS checks now have a timeout in case a DNS server is not responding, so the checks don't stall indefinitely.
|
||||
* Better messages if external DNS is used and, weirdly, custom secondary nameservers are set.
|
||||
* Add POP to the mail client settings documentation.
|
||||
* The box's IP address is added to the fail2ban whitelist so that the status checks don't trigger the machine banning itself, which results in the status checks showing services down even though they are running.
|
||||
* For SSL certificates, rather than asking you what country you are in during setup, ask at the time a CSR is generated. The default system self-signed certificate now omits a country in the subject (it was never needed). The CSR_COUNTRY Mail-in-a-Box setting is dropped entirely.
|
||||
|
||||
System:
|
||||
|
||||
* Nightly backups and system status checks are now moved to 3am in the system's timezone.
|
||||
* fail2ban's recidive jail is now active, which guards against persistent brute force login attacks over long periods of time.
|
||||
* Setup (first run only) now asks for your timezone to set the system time.
|
||||
* The Exchange/ActiveSync server is now taken offline during nightly backups (along with SMTP and IMAP).
|
||||
* The machine's random number generator (/dev/urandom) is now seeded with Ubuntu Pollinate and a blocking read on /dev/random.
|
||||
* DNSSEC key generation during install now uses /dev/urandom (instead of /dev/random), which is faster.
|
||||
* The $STORAGE_ROOT/ssl directory is flattened by a migration script and the system SSL certificate path is now a symlink to the actual certificate.
|
||||
* If ownCloud sends out email, it will use the box's administrative address now (admin@yourboxname).
|
||||
* Z-Push (Exchange/ActiveSync) logs now exclude warnings and are now rotated to save disk space.
|
||||
* Fix pip command that might have not installed all necessary Python packages.
|
||||
* The control panel and backup would not work on Google Compute Engine because GCE installs a conflicting boto package.
|
||||
* Added a new command `management/backup.py --restore` to restore files from a backup to a target directory (command line arguments are passed to `duplicity restore`).
|
||||
|
||||
v0.14 (November 4, 2015)
|
||||
------------------------
|
||||
|
||||
@@ -370,7 +404,7 @@ v0.02 (September 21, 2014)
|
||||
* Better logic for determining when to take a full backup.
|
||||
* Reduce DNS TTL, not that it seems to really matter.
|
||||
* Add SSHFP DNS records.
|
||||
* Add an API for setting custom DNS records
|
||||
* Add an API for setting custom DNS records
|
||||
* Update to ownCloud 7.0.2.
|
||||
* Some things were broken if the machine had an IPv6 address.
|
||||
* Use a dialogs library to ask users questions during setup.
|
||||
|
||||
@@ -59,7 +59,7 @@ by me:
|
||||
$ curl -s https://keybase.io/joshdata/key.asc | gpg --import
|
||||
gpg: key C10BDD81: public key "Joshua Tauberer <jt@occams.info>" imported
|
||||
|
||||
$ git verify-tag v0.14
|
||||
$ git verify-tag v0.15
|
||||
gpg: Signature made ..... using RSA key ID C10BDD81
|
||||
gpg: Good signature from "Joshua Tauberer <jt@occams.info>"
|
||||
gpg: WARNING: This key is not certified with a trusted signature!
|
||||
@@ -72,7 +72,7 @@ and on my [personal homepage](https://razor.occams.info/). (Of course, if this r
|
||||
|
||||
Checkout the tag corresponding to the most recent release:
|
||||
|
||||
$ git checkout v0.14
|
||||
$ git checkout v0.15
|
||||
|
||||
Begin the installation.
|
||||
|
||||
|
||||
1
Vagrantfile
vendored
1
Vagrantfile
vendored
@@ -22,7 +22,6 @@ Vagrant.configure("2") do |config|
|
||||
export PUBLIC_IP=auto
|
||||
export PUBLIC_IPV6=auto
|
||||
export PRIMARY_HOSTNAME=auto-easy
|
||||
export CSR_COUNTRY=US
|
||||
#export SKIP_NETWORK_CHECKS=1
|
||||
|
||||
# Start the setup script.
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# Fail2Ban configuration file for Mail-in-a-Box
|
||||
|
||||
[DEFAULT]
|
||||
# Whitelist our own IP addresses. 127.0.0.1/8 is the default. But our status checks
|
||||
# ping services over the public interface so we should whitelist that address of
|
||||
# ours too. The string is substituted during installation.
|
||||
ignoreip = 127.0.0.1/8 PUBLIC_IP
|
||||
|
||||
# JAILS
|
||||
|
||||
[ssh]
|
||||
@@ -17,3 +23,7 @@ enabled = true
|
||||
filter = dovecotimap
|
||||
findtime = 30
|
||||
maxretry = 20
|
||||
|
||||
[recidive]
|
||||
enabled = true
|
||||
maxretry = 10
|
||||
|
||||
@@ -12,7 +12,7 @@ import os, os.path, shutil, glob, re, datetime
|
||||
import dateutil.parser, dateutil.relativedelta, dateutil.tz
|
||||
import rtyaml
|
||||
|
||||
from utils import exclusive_process, load_environment, shell, wait_for_service
|
||||
from utils import exclusive_process, load_environment, shell, wait_for_service, fix_boto
|
||||
|
||||
def backup_status(env):
|
||||
# Root folder
|
||||
@@ -220,8 +220,9 @@ def perform_backup(full_backup):
|
||||
full_backup = full_backup or should_force_full(env)
|
||||
|
||||
# Stop services.
|
||||
shell('check_call', ["/usr/sbin/service", "dovecot", "stop"])
|
||||
shell('check_call', ["/usr/sbin/service", "php5-fpm", "stop"])
|
||||
shell('check_call', ["/usr/sbin/service", "postfix", "stop"])
|
||||
shell('check_call', ["/usr/sbin/service", "dovecot", "stop"])
|
||||
|
||||
# Run a backup of STORAGE_ROOT (but excluding the backups themselves!).
|
||||
# --allow-source-mismatch is needed in case the box's hostname is changed
|
||||
@@ -243,6 +244,7 @@ def perform_backup(full_backup):
|
||||
# Start services again.
|
||||
shell('check_call', ["/usr/sbin/service", "dovecot", "start"])
|
||||
shell('check_call', ["/usr/sbin/service", "postfix", "start"])
|
||||
shell('check_call', ["/usr/sbin/service", "php5-fpm", "start"])
|
||||
|
||||
# Once the migrated backup is included in a new backup, it can be deleted.
|
||||
if os.path.isdir(migrated_unencrypted_backup_dir):
|
||||
@@ -312,6 +314,18 @@ def run_duplicity_verification():
|
||||
env["STORAGE_ROOT"],
|
||||
], get_env(env))
|
||||
|
||||
def run_duplicity_restore(args):
|
||||
env = load_environment()
|
||||
config = get_backup_config(env)
|
||||
backup_cache_dir = os.path.join(env["STORAGE_ROOT"], 'backup', 'cache')
|
||||
shell('check_call', [
|
||||
"/usr/bin/duplicity",
|
||||
"restore",
|
||||
"--archive-dir", backup_cache_dir,
|
||||
config["target"],
|
||||
] + args,
|
||||
get_env(env))
|
||||
|
||||
def list_target_files(config):
|
||||
import urllib.parse
|
||||
try:
|
||||
@@ -324,6 +338,7 @@ def list_target_files(config):
|
||||
|
||||
elif p.scheme == "s3":
|
||||
# match to a Region
|
||||
fix_boto() # must call prior to importing boto
|
||||
import boto.s3
|
||||
from boto.exception import BotoServerError
|
||||
for region in boto.s3.regions():
|
||||
@@ -435,6 +450,16 @@ if __name__ == "__main__":
|
||||
# are readable, and b) report if they are up to date.
|
||||
run_duplicity_verification()
|
||||
|
||||
elif sys.argv[-1] == "--status":
|
||||
# Show backup status.
|
||||
ret = backup_status(load_environment())
|
||||
print(rtyaml.dump(ret["backups"]))
|
||||
|
||||
elif len(sys.argv) >= 2 and sys.argv[1] == "--restore":
|
||||
# Run duplicity restore. Rest of command line passed as arguments
|
||||
# to duplicity. The restore path should be specified.
|
||||
run_duplicity_restore(sys.argv[2:])
|
||||
|
||||
else:
|
||||
# Perform a backup. Add --full to force a full backup rather than
|
||||
# possibly performing an incremental backup.
|
||||
|
||||
@@ -1,27 +1,28 @@
|
||||
# This list is derived from https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2.
|
||||
# The columns are ISO_3166-1_alpha-2 code, display name, Wikipedia page name.
|
||||
# The top 20 countries by number of Internet users are grouped first, see
|
||||
# The top 21 countries by number of Internet users are grouped first, see
|
||||
# https://en.wikipedia.org/wiki/List_of_countries_by_number_of_Internet_users.
|
||||
BR Brazil
|
||||
CA Canada
|
||||
CN China
|
||||
EG Egypt
|
||||
FR France
|
||||
DE Germany
|
||||
IN India
|
||||
ID Indonesia
|
||||
IT Italy
|
||||
JP Japan
|
||||
MX Mexico
|
||||
NG Nigeria
|
||||
PH Philippines
|
||||
RU Russian Federation Russia
|
||||
ES Spain
|
||||
KR South Korea
|
||||
TR Turkey
|
||||
GB United Kingdom
|
||||
US United States
|
||||
JP Japan
|
||||
BR Brazil
|
||||
RU Russian Federation Russia
|
||||
DE Germany
|
||||
NG Nigeria
|
||||
GB United Kingdom
|
||||
FR France
|
||||
MX Mexico
|
||||
EG Egypt
|
||||
KR South Korea
|
||||
VN Vietnam
|
||||
ID Indonesia
|
||||
PH Philippines
|
||||
TR Turkey
|
||||
IT Italy
|
||||
PK Pakistan
|
||||
ES Spain
|
||||
CA Canada
|
||||
AD Andorra
|
||||
AE United Arab Emirates
|
||||
AF Afghanistan
|
||||
@@ -183,7 +184,6 @@ PA Panama
|
||||
PE Peru
|
||||
PF French Polynesia
|
||||
PG Papua New Guinea
|
||||
PK Pakistan
|
||||
PL Poland
|
||||
PM Saint Pierre and Miquelon
|
||||
PN Pitcairn Pitcairn Islands
|
||||
|
@@ -28,6 +28,14 @@ try:
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
# for generating CSRs we need a list of country codes
|
||||
csr_country_codes = []
|
||||
with open(os.path.join(os.path.dirname(me), "csr_country_codes.tsv")) as f:
|
||||
for line in f:
|
||||
if line.strip() == "" or line.startswith("#"): continue
|
||||
code, name = line.strip().split("\t")[0:2]
|
||||
csr_country_codes.append((code, name))
|
||||
|
||||
app = Flask(__name__, template_folder=os.path.abspath(os.path.join(os.path.dirname(me), "templates")))
|
||||
|
||||
# Decorator to protect views that require a user with 'admin' privileges.
|
||||
@@ -94,15 +102,19 @@ def index():
|
||||
no_users_exist = (len(get_mail_users(env)) == 0)
|
||||
no_admins_exist = (len(get_admins(env)) == 0)
|
||||
|
||||
utils.fix_boto() # must call prior to importing boto
|
||||
import boto.s3
|
||||
backup_s3_hosts = [(r.name, r.endpoint) for r in boto.s3.regions()]
|
||||
|
||||
return render_template('index.html',
|
||||
hostname=env['PRIMARY_HOSTNAME'],
|
||||
storage_root=env['STORAGE_ROOT'],
|
||||
|
||||
no_users_exist=no_users_exist,
|
||||
no_admins_exist=no_admins_exist,
|
||||
|
||||
backup_s3_hosts=backup_s3_hosts,
|
||||
csr_country_codes=csr_country_codes,
|
||||
)
|
||||
|
||||
@app.route('/me')
|
||||
@@ -318,17 +330,20 @@ def dns_get_dump():
|
||||
@app.route('/ssl/csr/<domain>', methods=['POST'])
|
||||
@authorized_personnel_only
|
||||
def ssl_get_csr(domain):
|
||||
from web_update import create_csr
|
||||
from ssl_certificates import create_csr
|
||||
ssl_private_key = os.path.join(os.path.join(env["STORAGE_ROOT"], 'ssl', 'ssl_private_key.pem'))
|
||||
return create_csr(domain, ssl_private_key, env)
|
||||
return create_csr(domain, ssl_private_key, request.form.get('countrycode', ''), env)
|
||||
|
||||
@app.route('/ssl/install', methods=['POST'])
|
||||
@authorized_personnel_only
|
||||
def ssl_install_cert():
|
||||
from web_update import install_cert
|
||||
from web_update import get_web_domains
|
||||
from ssl_certificates import install_cert
|
||||
domain = request.form.get('domain')
|
||||
ssl_cert = request.form.get('cert')
|
||||
ssl_chain = request.form.get('chain')
|
||||
if domain not in get_web_domains(env):
|
||||
return "Invalid domain name."
|
||||
return install_cert(domain, ssl_cert, ssl_chain, env)
|
||||
|
||||
# WEB
|
||||
|
||||
8
management/daily_tasks.sh
Executable file
8
management/daily_tasks.sh
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
# This script is run daily (at 3am each night).
|
||||
|
||||
# Take a backup.
|
||||
management/backup.py
|
||||
|
||||
# Run status checks and email the administrator if anything changed.
|
||||
management/status_checks.py --show-changes --smtp
|
||||
@@ -51,21 +51,13 @@ def get_dns_zones(env):
|
||||
return zonefiles
|
||||
|
||||
def do_dns_update(env, force=False):
|
||||
# What domains (and their zone filenames) should we build?
|
||||
domains = get_dns_domains(env)
|
||||
zonefiles = get_dns_zones(env)
|
||||
|
||||
# Custom records to add to zones.
|
||||
additional_records = list(get_custom_dns_config(env))
|
||||
from web_update import get_default_www_redirects
|
||||
www_redirect_domains = get_default_www_redirects(env)
|
||||
|
||||
# Write zone files.
|
||||
os.makedirs('/etc/nsd/zones', exist_ok=True)
|
||||
zonefiles = []
|
||||
updated_domains = []
|
||||
for i, (domain, zonefile) in enumerate(zonefiles):
|
||||
# Build the records to put in the zone.
|
||||
records = build_zone(domain, domains, additional_records, www_redirect_domains, env)
|
||||
for (domain, zonefile, records) in build_zones(env):
|
||||
# The final set of files will be signed.
|
||||
zonefiles.append((domain, zonefile + ".signed"))
|
||||
|
||||
# See if the zone has changed, and if so update the serial number
|
||||
# and write the zone file.
|
||||
@@ -73,14 +65,6 @@ def do_dns_update(env, force=False):
|
||||
# Zone was not updated. There were no changes.
|
||||
continue
|
||||
|
||||
# If this is a .justtesting.email domain, then post the update.
|
||||
try:
|
||||
justtestingdotemail(domain, records)
|
||||
except:
|
||||
# Hmm. Might be a network issue. If we stop now, will we end
|
||||
# up in an inconsistent state? Let's just continue.
|
||||
pass
|
||||
|
||||
# Mark that we just updated this domain.
|
||||
updated_domains.append(domain)
|
||||
|
||||
@@ -95,14 +79,8 @@ def do_dns_update(env, force=False):
|
||||
# and return True so we get a chance to re-sign it.
|
||||
sign_zone(domain, zonefile, env)
|
||||
|
||||
# Now that all zones are signed (some might not have changed and so didn't
|
||||
# just get signed now, but were before) update the zone filename so nsd.conf
|
||||
# uses the signed file.
|
||||
for i in range(len(zonefiles)):
|
||||
zonefiles[i][1] += ".signed"
|
||||
|
||||
# Write the main nsd.conf file.
|
||||
if write_nsd_conf(zonefiles, additional_records, env):
|
||||
if write_nsd_conf(zonefiles, list(get_custom_dns_config(env)), env):
|
||||
# Make sure updated_domains contains *something* if we wrote an updated
|
||||
# nsd.conf so that we know to restart nsd.
|
||||
if len(updated_domains) == 0:
|
||||
@@ -112,8 +90,8 @@ def do_dns_update(env, force=False):
|
||||
if len(updated_domains) > 0:
|
||||
shell('check_call', ["/usr/sbin/service", "nsd", "restart"])
|
||||
|
||||
# Write the OpenDKIM configuration tables.
|
||||
if write_opendkim_tables(domains, env):
|
||||
# Write the OpenDKIM configuration tables for all of the domains.
|
||||
if write_opendkim_tables([domain for domain, zonefile in zonefiles], env):
|
||||
# Settings changed. Kick opendkim.
|
||||
shell('check_call', ["/usr/sbin/service", "opendkim", "restart"])
|
||||
if len(updated_domains) == 0:
|
||||
@@ -132,6 +110,22 @@ def do_dns_update(env, force=False):
|
||||
|
||||
########################################################################
|
||||
|
||||
def build_zones(env):
|
||||
# What domains (and their zone filenames) should we build?
|
||||
domains = get_dns_domains(env)
|
||||
zonefiles = get_dns_zones(env)
|
||||
|
||||
# Custom records to add to zones.
|
||||
additional_records = list(get_custom_dns_config(env))
|
||||
from web_update import get_web_domains
|
||||
www_redirect_domains = set(get_web_domains(env)) - set(get_web_domains(env, include_www_redirects=False))
|
||||
|
||||
# Build DNS records for each zone.
|
||||
for domain, zonefile in zonefiles:
|
||||
# Build the records to put in the zone.
|
||||
records = build_zone(domain, domains, additional_records, www_redirect_domains, env)
|
||||
yield (domain, zonefile, records)
|
||||
|
||||
def build_zone(domain, all_domains, additional_records, www_redirect_domains, env, is_zone=True):
|
||||
records = []
|
||||
|
||||
@@ -289,26 +283,40 @@ def build_zone(domain, all_domains, additional_records, www_redirect_domains, en
|
||||
|
||||
def build_tlsa_record(env):
|
||||
# A DANE TLSA record in DNS specifies that connections on a port
|
||||
# must use TLS and the certificate must match a particular certificate.
|
||||
# must use TLS and the certificate must match a particular criteria.
|
||||
#
|
||||
# Thanks to http://blog.huque.com/2012/10/dnssec-and-certificates.html
|
||||
# for explaining all of this!
|
||||
# and https://community.letsencrypt.org/t/please-avoid-3-0-1-and-3-0-2-dane-tlsa-records-with-le-certificates/7022
|
||||
# for explaining all of this! Also see https://tools.ietf.org/html/rfc6698#section-2.1
|
||||
# and https://github.com/mail-in-a-box/mailinabox/issues/268#issuecomment-167160243.
|
||||
#
|
||||
# There are several criteria. We used to use "3 0 1" criteria, which
|
||||
# meant to pin a leaf (3) certificate (0) with SHA256 hash (1). But
|
||||
# certificates change, and especially as we move to short-lived certs
|
||||
# they change often. The TLSA record handily supports the criteria of
|
||||
# a leaf certificate (3)'s subject public key (1) with SHA256 hash (1).
|
||||
# The subject public key is the public key portion of the private key
|
||||
# that generated the CSR that generated the certificate. Since we
|
||||
# generate a private key once the first time Mail-in-a-Box is set up
|
||||
# and reuse it for all subsequent certificates, the TLSA record will
|
||||
# remain valid indefinitely.
|
||||
|
||||
# Get the hex SHA256 of the DER-encoded server certificate:
|
||||
certder = shell("check_output", [
|
||||
"/usr/bin/openssl",
|
||||
"x509",
|
||||
"-in", os.path.join(env["STORAGE_ROOT"], "ssl", "ssl_certificate.pem"),
|
||||
"-outform", "DER"
|
||||
],
|
||||
return_bytes=True)
|
||||
certhash = hashlib.sha256(certder).hexdigest()
|
||||
from ssl_certificates import load_cert_chain, load_pem
|
||||
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
|
||||
|
||||
fn = os.path.join(env["STORAGE_ROOT"], "ssl", "ssl_certificate.pem")
|
||||
cert = load_pem(load_cert_chain(fn)[0])
|
||||
|
||||
subject_public_key = cert.public_key().public_bytes(Encoding.DER, PublicFormat.SubjectPublicKeyInfo)
|
||||
# We could have also loaded ssl_private_key.pem and called priv_key.public_key().public_bytes(...)
|
||||
|
||||
pk_hash = hashlib.sha256(subject_public_key).hexdigest()
|
||||
|
||||
# Specify the TLSA parameters:
|
||||
# 3: This is the certificate that the client should trust. No CA is needed.
|
||||
# 0: The whole certificate is matched.
|
||||
# 1: The certificate is SHA256'd here.
|
||||
return "3 0 1 " + certhash
|
||||
# 3: Match the (leaf) certificate. (No CA, no trust path needed.)
|
||||
# 1: Match its subject public key.
|
||||
# 1: Use SHA256.
|
||||
return "3 1 1 " + pk_hash
|
||||
|
||||
def build_sshfp_records():
|
||||
# The SSHFP record is a way for us to embed this server's SSH public
|
||||
@@ -794,6 +802,7 @@ def set_custom_dns_record(qname, rtype, value, action, env):
|
||||
|
||||
def get_secondary_dns(custom_dns, mode=None):
|
||||
resolver = dns.resolver.get_default_resolver()
|
||||
resolver.timeout = 10
|
||||
|
||||
values = []
|
||||
for qname, rtype, value in custom_dns:
|
||||
@@ -826,11 +835,12 @@ def set_secondary_dns(hostnames, env):
|
||||
if len(hostnames) > 0:
|
||||
# Validate that all hostnames are valid and that all zone-xfer IP addresses are valid.
|
||||
resolver = dns.resolver.get_default_resolver()
|
||||
resolver.timeout = 5
|
||||
for item in hostnames:
|
||||
if not item.startswith("xfr:"):
|
||||
# Resolve hostname.
|
||||
try:
|
||||
response = dns.resolver.query(item, "A")
|
||||
response = resolver.query(item, "A")
|
||||
except (dns.resolver.NoNameservers, dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
|
||||
raise ValueError("Could not resolve the IP address of %s." % item)
|
||||
else:
|
||||
@@ -859,57 +869,9 @@ def get_custom_dns_record(custom_dns, qname, rtype):
|
||||
|
||||
########################################################################
|
||||
|
||||
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.
|
||||
# Ideally if dns4e.com supported NS records we would just have it
|
||||
# delegate DNS to us, but instead we will populate the whole zone.
|
||||
|
||||
import subprocess, json, urllib.parse
|
||||
|
||||
if not domain.endswith(".justtesting.email"):
|
||||
return
|
||||
|
||||
for subdomain, querytype, value, explanation in records:
|
||||
if querytype in ("NS",): continue
|
||||
if subdomain in ("www", "ns1", "ns2"): continue # don't do unnecessary things
|
||||
|
||||
if subdomain == None:
|
||||
subdomain = domain
|
||||
else:
|
||||
subdomain = subdomain + "." + domain
|
||||
|
||||
if querytype == "TXT":
|
||||
# nsd requires parentheses around txt records with multiple parts,
|
||||
# but DNS4E requires there be no parentheses; also it goes into
|
||||
# nsd with a newline and a tab, which we replace with a space here
|
||||
value = re.sub("^\s*\(\s*([\w\W]*)\)", r"\1", value)
|
||||
value = re.sub("\s+", " ", value)
|
||||
else:
|
||||
continue
|
||||
|
||||
print("Updating DNS for %s/%s..." % (subdomain, querytype))
|
||||
resp = json.loads(subprocess.check_output([
|
||||
"curl",
|
||||
"-s",
|
||||
"https://api.dns4e.com/v7/%s/%s" % (urllib.parse.quote(subdomain), querytype.lower()),
|
||||
"--user", "2ddbd8e88ed1495fa0ec:A97TDJV26CVUJS6hqAs0CKnhj4HvjTM7MwAAg8xb",
|
||||
"--data", "record=%s" % urllib.parse.quote(value),
|
||||
]).decode("utf8"))
|
||||
print("\t...", resp.get("message", "?"))
|
||||
|
||||
########################################################################
|
||||
|
||||
def build_recommended_dns(env):
|
||||
ret = []
|
||||
domains = get_dns_domains(env)
|
||||
zonefiles = get_dns_zones(env)
|
||||
additional_records = list(get_custom_dns_config(env))
|
||||
from web_update import get_default_www_redirects
|
||||
www_redirect_domains = get_default_www_redirects(env)
|
||||
for domain, zonefile in zonefiles:
|
||||
records = build_zone(domain, domains, additional_records, www_redirect_domains, env)
|
||||
|
||||
for (domain, zonefile, records) in build_zones(env):
|
||||
# remove records that we don't dislay
|
||||
records = [r for r in records if r[3] is not False]
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ def prettify_idn_email_address(email):
|
||||
|
||||
def is_dcv_address(email):
|
||||
email = email.lower()
|
||||
for localpart in ("admin", "administrator", "postmaster", "hostmaster", "webmaster"):
|
||||
for localpart in ("admin", "administrator", "postmaster", "hostmaster", "webmaster", "abuse"):
|
||||
if email.startswith(localpart+"@") or email.startswith(localpart+"+"):
|
||||
return True
|
||||
return False
|
||||
@@ -520,17 +520,21 @@ def get_required_aliases(env):
|
||||
# email on that domain are the required aliases or a catch-all/domain-forwarder.
|
||||
real_mail_domains = get_mail_domains(env,
|
||||
filter_aliases = lambda alias :
|
||||
not alias.startswith("postmaster@") and not alias.startswith("admin@")
|
||||
not alias.startswith("postmaster@")
|
||||
and not alias.startswith("admin@")
|
||||
and not alias.startswith("abuse@")
|
||||
and not alias.startswith("@")
|
||||
)
|
||||
|
||||
# Create postmaster@ and admin@ for all domains we serve mail on.
|
||||
# postmaster@ is assumed to exist by our Postfix configuration. admin@
|
||||
# isn't anything, but it might save the user some trouble e.g. when
|
||||
# Create postmaster@, admin@ and abuse@ for all domains we serve
|
||||
# mail on. postmaster@ is assumed to exist by our Postfix configuration.
|
||||
# admin@isn't anything, but it might save the user some trouble e.g. when
|
||||
# buying an SSL certificate.
|
||||
# abuse@ is part of RFC2142: https://www.ietf.org/rfc/rfc2142.txt
|
||||
for domain in real_mail_domains:
|
||||
aliases.add("postmaster@" + domain)
|
||||
aliases.add("admin@" + domain)
|
||||
aliases.add("abuse@" + domain)
|
||||
|
||||
return aliases
|
||||
|
||||
@@ -572,7 +576,7 @@ def kick(env, mail_result=None):
|
||||
# longer have any other email addresses for.
|
||||
for address, forwards_to, *_ in existing_alias_records:
|
||||
user, domain = address.split("@")
|
||||
if user in ("postmaster", "admin") \
|
||||
if user in ("postmaster", "admin", "abuse") \
|
||||
and address not in required_aliases \
|
||||
and forwards_to == get_system_administrator(env):
|
||||
remove_mail_alias(address, env, do_kick=False)
|
||||
|
||||
383
management/ssl_certificates.py
Normal file
383
management/ssl_certificates.py
Normal file
@@ -0,0 +1,383 @@
|
||||
# Utilities for installing and selecting SSL certificates.
|
||||
|
||||
import os, os.path, re, shutil
|
||||
|
||||
from utils import shell, safe_domain_name
|
||||
|
||||
def get_ssl_certificates(env):
|
||||
# Scan all of the installed SSL certificates and map every domain
|
||||
# that the certificates are good for to the best certificate for
|
||||
# the domain.
|
||||
|
||||
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
|
||||
from cryptography.x509 import Certificate
|
||||
|
||||
# The certificates are all stored here:
|
||||
ssl_root = os.path.join(env["STORAGE_ROOT"], 'ssl')
|
||||
|
||||
# List all of the files in the SSL directory and one level deep.
|
||||
def get_file_list():
|
||||
for fn in os.listdir(ssl_root):
|
||||
fn = os.path.join(ssl_root, fn)
|
||||
if os.path.isfile(fn):
|
||||
yield fn
|
||||
elif os.path.isdir(fn):
|
||||
for fn1 in os.listdir(fn):
|
||||
fn1 = os.path.join(fn, fn1)
|
||||
if os.path.isfile(fn1):
|
||||
yield fn1
|
||||
|
||||
# Remember stuff.
|
||||
private_keys = { }
|
||||
certificates = [ ]
|
||||
|
||||
# Scan each of the files to find private keys and certificates.
|
||||
# We must load all of the private keys first before processing
|
||||
# certificates so that we can check that we have a private key
|
||||
# available before using a certificate.
|
||||
for fn in get_file_list():
|
||||
try:
|
||||
pem = load_pem(load_cert_chain(fn)[0])
|
||||
except ValueError:
|
||||
# Not a valid PEM format for a PEM type we care about.
|
||||
continue
|
||||
|
||||
# Remember where we got this object.
|
||||
pem._filename = fn
|
||||
|
||||
# Is it a private key?
|
||||
if isinstance(pem, RSAPrivateKey):
|
||||
private_keys[pem.public_key().public_numbers()] = pem
|
||||
|
||||
# Is it a certificate?
|
||||
if isinstance(pem, Certificate):
|
||||
certificates.append(pem)
|
||||
|
||||
# Process the certificates.
|
||||
domains = { }
|
||||
for cert in certificates:
|
||||
# What domains is this certificate good for?
|
||||
cert_domains, primary_domain = get_certificate_domains(cert)
|
||||
cert._primary_domain = primary_domain
|
||||
|
||||
# Is there a private key file for this certificate?
|
||||
private_key = private_keys.get(cert.public_key().public_numbers())
|
||||
if not private_key:
|
||||
continue
|
||||
cert._private_key = private_key
|
||||
|
||||
# Add this cert to the list of certs usable for the domains.
|
||||
for domain in cert_domains:
|
||||
domains.setdefault(domain, []).append(cert)
|
||||
|
||||
# Sort the certificates to prefer good ones.
|
||||
import datetime
|
||||
now = datetime.datetime.utcnow()
|
||||
ret = { }
|
||||
for domain, cert_list in domains.items():
|
||||
cert_list.sort(key = lambda cert : (
|
||||
# must be valid NOW
|
||||
cert.not_valid_before <= now <= cert.not_valid_after,
|
||||
|
||||
# prefer one that is not self-signed
|
||||
cert.issuer != cert.subject,
|
||||
|
||||
# prefer one with the expiration furthest into the future so
|
||||
# that we can easily rotate to new certs as we get them
|
||||
cert.not_valid_after,
|
||||
|
||||
# in case a certificate is installed in multiple paths,
|
||||
# prefer the... lexicographically last one?
|
||||
cert._filename,
|
||||
|
||||
), reverse=True)
|
||||
cert = cert_list.pop(0)
|
||||
ret[domain] = {
|
||||
"private-key": cert._private_key._filename,
|
||||
"certificate": cert._filename,
|
||||
"primary-domain": cert._primary_domain,
|
||||
}
|
||||
|
||||
return ret
|
||||
|
||||
def get_domain_ssl_files(domain, ssl_certificates, env, allow_missing_cert=False):
|
||||
# Get the default paths.
|
||||
ssl_private_key = os.path.join(os.path.join(env["STORAGE_ROOT"], 'ssl', 'ssl_private_key.pem'))
|
||||
ssl_certificate = os.path.join(os.path.join(env["STORAGE_ROOT"], 'ssl', 'ssl_certificate.pem'))
|
||||
|
||||
if domain == env['PRIMARY_HOSTNAME']:
|
||||
# The primary domain must use the server certificate because
|
||||
# it is hard-coded in some service configuration files.
|
||||
return ssl_private_key, ssl_certificate, None
|
||||
|
||||
wildcard_domain = re.sub("^[^\.]+", "*", domain)
|
||||
|
||||
if domain in ssl_certificates:
|
||||
cert_info = ssl_certificates[domain]
|
||||
cert_type = "multi-domain"
|
||||
elif wildcard_domain in ssl_certificates:
|
||||
cert_info = ssl_certificates[wildcard_domain]
|
||||
cert_type = "wildcard"
|
||||
elif not allow_missing_cert:
|
||||
# No certificate is available for this domain! Return default files.
|
||||
ssl_via = "Using certificate for %s." % env['PRIMARY_HOSTNAME']
|
||||
return ssl_private_key, ssl_certificate, ssl_via
|
||||
else:
|
||||
# No certificate is available - and warn appropriately.
|
||||
return None
|
||||
|
||||
# 'via' is a hint to the user about which certificate is in use for the domain
|
||||
if cert_info['certificate'] == os.path.join(env["STORAGE_ROOT"], 'ssl', 'ssl_certificate.pem'):
|
||||
# Using the server certificate.
|
||||
via = "Using same %s certificate as for %s." % (cert_type, env['PRIMARY_HOSTNAME'])
|
||||
elif cert_info['primary-domain'] != domain and cert_info['primary-domain'] in ssl_certificates and cert_info == ssl_certificates[cert_info['primary-domain']]:
|
||||
via = "Using same %s certificate as for %s." % (cert_type, cert_info['primary-domain'])
|
||||
else:
|
||||
via = None # don't show a hint - show expiration info instead
|
||||
|
||||
return cert_info['private-key'], cert_info['certificate'], via
|
||||
|
||||
def create_csr(domain, ssl_key, country_code, env):
|
||||
return shell("check_output", [
|
||||
"openssl", "req", "-new",
|
||||
"-key", ssl_key,
|
||||
"-sha256",
|
||||
"-subj", "/C=%s/ST=/L=/O=/CN=%s" % (country_code, domain)])
|
||||
|
||||
def install_cert(domain, ssl_cert, ssl_chain, env):
|
||||
# Write the combined cert+chain to a temporary path and validate that it is OK.
|
||||
# The certificate always goes above the chain.
|
||||
import tempfile
|
||||
fd, fn = tempfile.mkstemp('.pem')
|
||||
os.write(fd, (ssl_cert + '\n' + ssl_chain).encode("ascii"))
|
||||
os.close(fd)
|
||||
|
||||
# Do validation on the certificate before installing it.
|
||||
ssl_private_key = os.path.join(os.path.join(env["STORAGE_ROOT"], 'ssl', 'ssl_private_key.pem'))
|
||||
cert_status, cert_status_details = check_certificate(domain, fn, ssl_private_key)
|
||||
if cert_status != "OK":
|
||||
if cert_status == "SELF-SIGNED":
|
||||
cert_status = "This is a self-signed certificate. I can't install that."
|
||||
os.unlink(fn)
|
||||
if cert_status_details is not None:
|
||||
cert_status += " " + cert_status_details
|
||||
return cert_status
|
||||
|
||||
# Where to put it?
|
||||
# Make a unique path for the certificate.
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from binascii import hexlify
|
||||
cert = load_pem(load_cert_chain(fn)[0])
|
||||
all_domains, cn = get_certificate_domains(cert)
|
||||
path = "%s-%s-%s.pem" % (
|
||||
safe_domain_name(cn), # common name, which should be filename safe because it is IDNA-encoded, but in case of a malformed cert make sure it's ok to use as a filename
|
||||
cert.not_valid_after.date().isoformat().replace("-", ""), # expiration date
|
||||
hexlify(cert.fingerprint(hashes.SHA256())).decode("ascii")[0:8], # fingerprint prefix
|
||||
)
|
||||
ssl_certificate = os.path.join(os.path.join(env["STORAGE_ROOT"], 'ssl', path))
|
||||
|
||||
# Install the certificate.
|
||||
os.makedirs(os.path.dirname(ssl_certificate), exist_ok=True)
|
||||
shutil.move(fn, ssl_certificate)
|
||||
|
||||
ret = ["OK"]
|
||||
|
||||
# When updating the cert for PRIMARY_HOSTNAME, symlink it from the system
|
||||
# certificate path, which is hard-coded for various purposes, and then
|
||||
# restart postfix and dovecot.
|
||||
if domain == env['PRIMARY_HOSTNAME']:
|
||||
# Update symlink.
|
||||
system_ssl_certificate = os.path.join(os.path.join(env["STORAGE_ROOT"], 'ssl', 'ssl_certificate.pem'))
|
||||
os.unlink(system_ssl_certificate)
|
||||
os.symlink(ssl_certificate, system_ssl_certificate)
|
||||
|
||||
# Restart postfix and dovecot so they pick up the new file.
|
||||
shell('check_call', ["/usr/sbin/service", "postfix", "restart"])
|
||||
shell('check_call', ["/usr/sbin/service", "dovecot", "restart"])
|
||||
ret.append("mail services restarted")
|
||||
|
||||
# The DANE TLSA record will remain valid so long as the private key
|
||||
# hasn't changed. We don't ever change the private key automatically.
|
||||
# If the user does it, they must manually update DNS.
|
||||
|
||||
# Update the web configuration so nginx picks up the new certificate file.
|
||||
from web_update import do_web_update
|
||||
ret.append( do_web_update(env) )
|
||||
return "\n".join(ret)
|
||||
|
||||
|
||||
def check_certificate(domain, ssl_certificate, ssl_private_key, warn_if_expiring_soon=True, rounded_time=False, just_check_domain=False):
|
||||
# Check that the ssl_certificate & ssl_private_key files are good
|
||||
# for the provided domain.
|
||||
|
||||
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
|
||||
from cryptography.x509 import Certificate
|
||||
|
||||
# The ssl_certificate file may contain a chain of certificates. We'll
|
||||
# need to split that up before we can pass anything to openssl or
|
||||
# parse them in Python. Parse it with the cryptography library.
|
||||
try:
|
||||
ssl_cert_chain = load_cert_chain(ssl_certificate)
|
||||
cert = load_pem(ssl_cert_chain[0])
|
||||
if not isinstance(cert, Certificate): raise ValueError("This is not a certificate file.")
|
||||
except ValueError as e:
|
||||
return ("There is a problem with the certificate file: %s" % str(e), None)
|
||||
|
||||
# First check that the domain name is one of the names allowed by
|
||||
# the certificate.
|
||||
if domain is not None:
|
||||
certificate_names, cert_primary_name = get_certificate_domains(cert)
|
||||
|
||||
# Check that the domain appears among the acceptable names, or a wildcard
|
||||
# form of the domain name (which is a stricter check than the specs but
|
||||
# should work in normal cases).
|
||||
wildcard_domain = re.sub("^[^\.]+", "*", domain)
|
||||
if domain not in certificate_names and wildcard_domain not in certificate_names:
|
||||
return ("The certificate is for the wrong domain name. It is for %s."
|
||||
% ", ".join(sorted(certificate_names)), None)
|
||||
|
||||
# Second, check that the certificate matches the private key.
|
||||
if ssl_private_key is not None:
|
||||
try:
|
||||
priv_key = load_pem(open(ssl_private_key, 'rb').read())
|
||||
except ValueError as e:
|
||||
return ("The private key file %s is not a private key file: %s" % (ssl_private_key, str(e)), None)
|
||||
|
||||
if not isinstance(priv_key, RSAPrivateKey):
|
||||
return ("The private key file %s is not a private key file." % ssl_private_key, None)
|
||||
|
||||
if priv_key.public_key().public_numbers() != cert.public_key().public_numbers():
|
||||
return ("The certificate does not correspond to the private key at %s." % ssl_private_key, None)
|
||||
|
||||
# We could also use the openssl command line tool to get the modulus
|
||||
# listed in each file. The output of each command below looks like "Modulus=XXXXX".
|
||||
# $ openssl rsa -inform PEM -noout -modulus -in ssl_private_key
|
||||
# $ openssl x509 -in ssl_certificate -noout -modulus
|
||||
|
||||
# Third, check if the certificate is self-signed. Return a special flag string.
|
||||
if cert.issuer == cert.subject:
|
||||
return ("SELF-SIGNED", None)
|
||||
|
||||
# When selecting which certificate to use for non-primary domains, we check if the primary
|
||||
# certificate or a www-parent-domain certificate is good for the domain. There's no need
|
||||
# to run extra checks beyond this point.
|
||||
if just_check_domain:
|
||||
return ("OK", None)
|
||||
|
||||
# Check that the certificate hasn't expired. The datetimes returned by the
|
||||
# certificate are 'naive' and in UTC. We need to get the current time in UTC.
|
||||
import datetime
|
||||
now = datetime.datetime.utcnow()
|
||||
if not(cert.not_valid_before <= now <= cert.not_valid_after):
|
||||
return ("The certificate has expired or is not yet valid. It is valid from %s to %s." % (cert.not_valid_before, cert.not_valid_after), None)
|
||||
|
||||
# Next validate that the certificate is valid. This checks whether the certificate
|
||||
# is self-signed, that the chain of trust makes sense, that it is signed by a CA
|
||||
# that Ubuntu has installed on this machine's list of CAs, and I think that it hasn't
|
||||
# expired.
|
||||
|
||||
# The certificate chain has to be passed separately and is given via STDIN.
|
||||
# This command returns a non-zero exit status in most cases, so trap errors.
|
||||
retcode, verifyoutput = shell('check_output', [
|
||||
"openssl",
|
||||
"verify", "-verbose",
|
||||
"-purpose", "sslserver", "-policy_check",]
|
||||
+ ([] if len(ssl_cert_chain) == 1 else ["-untrusted", "/proc/self/fd/0"])
|
||||
+ [ssl_certificate],
|
||||
input=b"\n\n".join(ssl_cert_chain[1:]),
|
||||
trap=True)
|
||||
|
||||
if "self signed" in verifyoutput:
|
||||
# Certificate is self-signed. Probably we detected this above.
|
||||
return ("SELF-SIGNED", None)
|
||||
|
||||
elif retcode != 0:
|
||||
if "unable to get local issuer certificate" in verifyoutput:
|
||||
return ("The certificate is missing an intermediate chain or the intermediate chain is incorrect or incomplete. (%s)" % verifyoutput, None)
|
||||
|
||||
# There is some unknown problem. Return the `openssl verify` raw output.
|
||||
return ("There is a problem with the SSL certificate.", verifyoutput.strip())
|
||||
|
||||
else:
|
||||
# `openssl verify` returned a zero exit status so the cert is currently
|
||||
# good.
|
||||
|
||||
# But is it expiring soon?
|
||||
cert_expiration_date = cert.not_valid_after
|
||||
ndays = (cert_expiration_date-now).days
|
||||
if not rounded_time or ndays < 7:
|
||||
expiry_info = "The certificate expires in %d days on %s." % (ndays, cert_expiration_date.strftime("%x"))
|
||||
elif ndays <= 14:
|
||||
expiry_info = "The certificate expires in less than two weeks, on %s." % cert_expiration_date.strftime("%x")
|
||||
elif ndays <= 31:
|
||||
expiry_info = "The certificate expires in less than a month, on %s." % cert_expiration_date.strftime("%x")
|
||||
else:
|
||||
expiry_info = "The certificate expires on %s." % cert_expiration_date.strftime("%x")
|
||||
|
||||
if ndays <= 31 and warn_if_expiring_soon:
|
||||
return ("The certificate is expiring soon: " + expiry_info, None)
|
||||
|
||||
# Return the special OK code.
|
||||
return ("OK", expiry_info)
|
||||
|
||||
def load_cert_chain(pemfile):
|
||||
# A certificate .pem file may contain a chain of certificates.
|
||||
# Load the file and split them apart.
|
||||
re_pem = rb"(-+BEGIN (?:.+)-+[\r\n]+(?:[A-Za-z0-9+/=]{1,64}[\r\n]+)+-+END (?:.+)-+[\r\n]+)"
|
||||
with open(pemfile, "rb") as f:
|
||||
pem = f.read() + b"\n" # ensure trailing newline
|
||||
pemblocks = re.findall(re_pem, pem)
|
||||
if len(pemblocks) == 0:
|
||||
raise ValueError("File does not contain valid PEM data.")
|
||||
return pemblocks
|
||||
|
||||
def load_pem(pem):
|
||||
# Parse a "---BEGIN .... END---" PEM string and return a Python object for it
|
||||
# using classes from the cryptography package.
|
||||
from cryptography.x509 import load_pem_x509_certificate
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
pem_type = re.match(b"-+BEGIN (.*?)-+[\r\n]", pem)
|
||||
if pem_type is None:
|
||||
raise ValueError("File is not a valid PEM-formatted file.")
|
||||
pem_type = pem_type.group(1)
|
||||
if pem_type in (b"RSA PRIVATE KEY", b"PRIVATE KEY"):
|
||||
return serialization.load_pem_private_key(pem, password=None, backend=default_backend())
|
||||
if pem_type == b"CERTIFICATE":
|
||||
return load_pem_x509_certificate(pem, default_backend())
|
||||
raise ValueError("Unsupported PEM object type: " + pem_type.decode("ascii", "replace"))
|
||||
|
||||
def get_certificate_domains(cert):
|
||||
from cryptography.x509 import DNSName, ExtensionNotFound, OID_COMMON_NAME, OID_SUBJECT_ALTERNATIVE_NAME
|
||||
import idna
|
||||
|
||||
names = set()
|
||||
cn = None
|
||||
|
||||
# The domain may be found in the Subject Common Name (CN). This comes back as an IDNA (ASCII)
|
||||
# string, which is the format we store domains in - so good.
|
||||
try:
|
||||
cn = cert.subject.get_attributes_for_oid(OID_COMMON_NAME)[0].value
|
||||
names.add(cn)
|
||||
except IndexError:
|
||||
# No common name? Certificate is probably generated incorrectly.
|
||||
# But we'll let it error-out when it doesn't find the domain.
|
||||
pass
|
||||
|
||||
# ... or be one of the Subject Alternative Names. The cryptography library handily IDNA-decodes
|
||||
# the names for us. We must encode back to ASCII, but wildcard certificates can't pass through
|
||||
# IDNA encoding/decoding so we must special-case. See https://github.com/pyca/cryptography/pull/2071.
|
||||
def idna_decode_dns_name(dns_name):
|
||||
if dns_name.startswith("*."):
|
||||
return "*." + idna.encode(dns_name[2:]).decode('ascii')
|
||||
else:
|
||||
return idna.encode(dns_name).decode('ascii')
|
||||
|
||||
try:
|
||||
sans = cert.extensions.get_extension_for_oid(OID_SUBJECT_ALTERNATIVE_NAME).value.get_values_for_type(DNSName)
|
||||
for san in sans:
|
||||
names.add(idna_decode_dns_name(san))
|
||||
except ExtensionNotFound:
|
||||
pass
|
||||
|
||||
return names, cn
|
||||
@@ -4,8 +4,6 @@
|
||||
# SSL certificates have been signed, etc., and if not tells the user
|
||||
# what to do next.
|
||||
|
||||
__ALL__ = ['check_certificate']
|
||||
|
||||
import sys, os, os.path, re, subprocess, datetime, multiprocessing.pool
|
||||
|
||||
import dns.reversename, dns.resolver
|
||||
@@ -13,7 +11,8 @@ import dateutil.parser, dateutil.tz
|
||||
import idna
|
||||
|
||||
from dns_update import get_dns_zones, build_tlsa_record, get_custom_dns_config, get_secondary_dns, get_custom_dns_record
|
||||
from web_update import get_web_domains, get_default_www_redirects, get_ssl_certificates, get_domain_ssl_files, get_domains_with_a_records
|
||||
from web_update import get_web_domains, get_domains_with_a_records
|
||||
from ssl_certificates import get_ssl_certificates, get_domain_ssl_files, check_certificate
|
||||
from mailconfig import get_mail_domains, get_mail_aliases
|
||||
|
||||
from utils import shell, sort_domains, load_env_vars_from_file, load_settings
|
||||
@@ -104,45 +103,60 @@ def check_service(i, service, env):
|
||||
# Skip check (no port, e.g. no sshd).
|
||||
return (i, None, None, None)
|
||||
|
||||
import socket
|
||||
output = BufferedOutput()
|
||||
running = False
|
||||
fatal = False
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.settimeout(1)
|
||||
try:
|
||||
try:
|
||||
s.connect((
|
||||
"127.0.0.1" if not service["public"] else env['PUBLIC_IP'],
|
||||
service["port"]))
|
||||
running = True
|
||||
except OSError as e1:
|
||||
if service["public"] and service["port"] != 53:
|
||||
# For public services (except DNS), try the private IP as a fallback.
|
||||
s1 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s1.settimeout(1)
|
||||
try:
|
||||
s1.connect(("127.0.0.1", service["port"]))
|
||||
output.print_error("%s is running but is not publicly accessible at %s:%d (%s)." % (service['name'], env['PUBLIC_IP'], service['port'], str(e1)))
|
||||
except:
|
||||
raise e1
|
||||
finally:
|
||||
s1.close()
|
||||
else:
|
||||
raise
|
||||
|
||||
except OSError as e:
|
||||
output.print_error("%s is not running (%s; port %d)." % (service['name'], str(e), service['port']))
|
||||
# Helper function to make a connection to the service, since we try
|
||||
# up to three ways (localhost, IPv4 address, IPv6 address).
|
||||
def try_connect(ip):
|
||||
# Connect to the given IP address on the service's port with a one-second timeout.
|
||||
import socket
|
||||
s = socket.socket(socket.AF_INET if ":" not in ip else socket.AF_INET6, socket.SOCK_STREAM)
|
||||
s.settimeout(1)
|
||||
try:
|
||||
s.connect((ip, service["port"]))
|
||||
return True
|
||||
except OSError as e:
|
||||
# timed out or some other odd error
|
||||
return False
|
||||
finally:
|
||||
s.close()
|
||||
|
||||
if service["public"]:
|
||||
# Service should be publicly accessible.
|
||||
if try_connect(env["PUBLIC_IP"]):
|
||||
# IPv4 ok.
|
||||
if not env.get("PUBLIC_IPV6") or service.get("ipv6") is False or try_connect(env["PUBLIC_IPV6"]):
|
||||
# No IPv6, or service isn't meant to run on IPv6, or IPv6 is good.
|
||||
running = True
|
||||
|
||||
# IPv4 ok but IPv6 failed. Try the PRIVATE_IPV6 address to see if the service is bound to the interface.
|
||||
elif service["port"] != 53 and try_connect(env["PRIVATE_IPV6"]):
|
||||
output.print_error("%s is running (and available over IPv4 and the local IPv6 address), but it is not publicly accessible at %s:%d." % (service['name'], env['PUBLIC_IP'], service['port']))
|
||||
else:
|
||||
output.print_error("%s is running and available over IPv4 but is not accessible over IPv6 at %s port %d." % (service['name'], env['PUBLIC_IPV6'], service['port']))
|
||||
|
||||
# IPv4 failed. Try the private IP to see if the service is running but not accessible (except DNS because a different service runs on the private IP).
|
||||
elif service["port"] != 53 and try_connect("127.0.0.1"):
|
||||
output.print_error("%s is running but is not publicly accessible at %s:%d." % (service['name'], env['PUBLIC_IP'], service['port']))
|
||||
else:
|
||||
output.print_error("%s is not running (port %d)." % (service['name'], service['port']))
|
||||
|
||||
# Why is nginx not running?
|
||||
if service["port"] in (80, 443):
|
||||
if not running and service["port"] in (80, 443):
|
||||
output.print_line(shell('check_output', ['nginx', '-t'], capture_stderr=True, trap=True)[1].strip())
|
||||
|
||||
# Flag if local DNS is not running.
|
||||
if service["port"] == 53 and service["public"] == False:
|
||||
fatal = True
|
||||
finally:
|
||||
s.close()
|
||||
else:
|
||||
# Service should be running locally.
|
||||
if try_connect("127.0.0.1"):
|
||||
running = True
|
||||
else:
|
||||
output.print_error("%s is not running (port %d)." % (service['name'], service['port']))
|
||||
|
||||
# Flag if local DNS is not running.
|
||||
if not running and service["port"] == 53 and service["public"] == False:
|
||||
fatal = True
|
||||
|
||||
return (i, running, fatal, output)
|
||||
|
||||
@@ -241,7 +255,7 @@ def run_domain_checks(rounded_time, env, output, pool):
|
||||
dns_domains = set(dns_zonefiles)
|
||||
|
||||
# Get the list of domains we serve HTTPS for.
|
||||
web_domains = set(get_web_domains(env) + get_default_www_redirects(env))
|
||||
web_domains = set(get_web_domains(env))
|
||||
|
||||
domains_to_check = mail_domains | dns_domains | web_domains
|
||||
|
||||
@@ -302,6 +316,7 @@ def check_primary_hostname_dns(domain, env, output, dns_domains, dns_zonefiles):
|
||||
|
||||
ip = query_dns(domain, "A")
|
||||
ns_ips = query_dns("ns1." + domain, "A") + '/' + query_dns("ns2." + domain, "A")
|
||||
my_ips = env['PUBLIC_IP'] + ((" / "+env['PUBLIC_IPV6']) if env.get("PUBLIC_IPV6") else "")
|
||||
|
||||
# Check that the ns1/ns2 hostnames resolve to A records. This information probably
|
||||
# comes from the TLD since the information is set at the registrar as glue records.
|
||||
@@ -324,24 +339,29 @@ def check_primary_hostname_dns(domain, env, output, dns_domains, dns_zonefiles):
|
||||
public DNS to update after a change."""
|
||||
% (env['PRIMARY_HOSTNAME'], env['PRIMARY_HOSTNAME'], env['PUBLIC_IP'], ns_ips))
|
||||
|
||||
# Check that PRIMARY_HOSTNAME resolves to PUBLIC_IP in public DNS.
|
||||
if ip == env['PUBLIC_IP']:
|
||||
output.print_ok("Domain resolves to box's IP address. [%s ↦ %s]" % (env['PRIMARY_HOSTNAME'], env['PUBLIC_IP']))
|
||||
# Check that PRIMARY_HOSTNAME resolves to PUBLIC_IP[V6] in public DNS.
|
||||
ipv6 = query_dns(domain, "AAAA") if env.get("PUBLIC_IPV6") else None
|
||||
if ip == env['PUBLIC_IP'] and ipv6 in (None, env['PUBLIC_IPV6']):
|
||||
output.print_ok("Domain resolves to box's IP address. [%s ↦ %s]" % (env['PRIMARY_HOSTNAME'], my_ips))
|
||||
else:
|
||||
output.print_error("""This domain must resolve to your box's IP address (%s) in public DNS but it currently resolves
|
||||
to %s. It may take several hours for public DNS to update after a change. This problem may result from other
|
||||
issues listed here."""
|
||||
% (env['PUBLIC_IP'], ip))
|
||||
issues listed above."""
|
||||
% (my_ips, ip + ((" / " + ipv6) if ipv6 is not None else "")))
|
||||
|
||||
# Check reverse DNS on the PRIMARY_HOSTNAME. Note that it might not be
|
||||
|
||||
# Check reverse DNS matches the PRIMARY_HOSTNAME. Note that it might not be
|
||||
# a DNS zone if it is a subdomain of another domain we have a zone for.
|
||||
ipaddr_rev = dns.reversename.from_address(env['PUBLIC_IP'])
|
||||
existing_rdns = query_dns(ipaddr_rev, "PTR")
|
||||
if existing_rdns == domain:
|
||||
output.print_ok("Reverse DNS is set correctly at ISP. [%s ↦ %s]" % (env['PUBLIC_IP'], env['PRIMARY_HOSTNAME']))
|
||||
else:
|
||||
existing_rdns_v4 = query_dns(dns.reversename.from_address(env['PUBLIC_IP']), "PTR")
|
||||
existing_rdns_v6 = query_dns(dns.reversename.from_address(env['PUBLIC_IPV6']), "PTR") if env.get("PUBLIC_IPV6") else None
|
||||
if existing_rdns_v4 == domain and existing_rdns_v6 in (None, domain):
|
||||
output.print_ok("Reverse DNS is set correctly at ISP. [%s ↦ %s]" % (my_ips, env['PRIMARY_HOSTNAME']))
|
||||
elif existing_rdns_v4 == existing_rdns_v6 or existing_rdns_v6 is None:
|
||||
output.print_error("""Your box's reverse DNS is currently %s, but it should be %s. Your ISP or cloud provider will have instructions
|
||||
on setting up reverse DNS for your box at %s.""" % (existing_rdns, domain, env['PUBLIC_IP']) )
|
||||
on setting up reverse DNS for your box.""" % (existing_rdns_v4, domain) )
|
||||
else:
|
||||
output.print_error("""Your box's reverse DNS is currently %s (IPv4) and %s (IPv6), but it should be %s. Your ISP or cloud provider will have instructions
|
||||
on setting up reverse DNS for your box.""" % (existing_rdns_v4, existing_rdns_v6, domain) )
|
||||
|
||||
# Check the TLSA record.
|
||||
tlsa_qname = "_25._tcp." + domain
|
||||
@@ -395,6 +415,8 @@ def check_dns_zone(domain, env, output, dns_zonefiles):
|
||||
correct_ns = "; ".join(sorted(["ns1." + env['PRIMARY_HOSTNAME']] + secondary_ns))
|
||||
ip = query_dns(domain, "A")
|
||||
|
||||
probably_external_dns = False
|
||||
|
||||
if existing_ns.lower() == correct_ns.lower():
|
||||
output.print_ok("Nameservers are set correctly at registrar. [%s]" % correct_ns)
|
||||
elif ip == correct_ip:
|
||||
@@ -402,6 +424,7 @@ def check_dns_zone(domain, env, output, dns_zonefiles):
|
||||
output.print_warning("""The nameservers set on this domain at your domain name registrar should be %s. They are currently %s.
|
||||
If you are using External DNS, this may be OK."""
|
||||
% (correct_ns, existing_ns) )
|
||||
probably_external_dns = True
|
||||
else:
|
||||
output.print_error("""The nameservers set on this domain are incorrect. They are currently %s. Use your domain name registrar's
|
||||
control panel to set the nameservers to %s."""
|
||||
@@ -409,7 +432,7 @@ def check_dns_zone(domain, env, output, dns_zonefiles):
|
||||
|
||||
# Check that each custom secondary nameserver resolves the IP address.
|
||||
|
||||
if custom_secondary_ns:
|
||||
if custom_secondary_ns and not probably_external_dns:
|
||||
for ns in custom_secondary_ns:
|
||||
# We must first resolve the nameserver to an IP address so we can query it.
|
||||
ns_ip = query_dns(ns, "A")
|
||||
@@ -591,6 +614,9 @@ def query_dns(qname, rtype, nxdomain='[Not Set]', at=None):
|
||||
resolver = dns.resolver.Resolver()
|
||||
resolver.nameservers = [at]
|
||||
|
||||
# Set a timeout so that a non-responsive server doesn't hold us back.
|
||||
resolver.timeout = 5
|
||||
|
||||
# Do the query.
|
||||
try:
|
||||
response = resolver.query(qname, rtype)
|
||||
@@ -663,181 +689,6 @@ def check_ssl_cert(domain, rounded_time, ssl_certificates, env, output):
|
||||
output.print_line(cert_status_details)
|
||||
output.print_line("")
|
||||
|
||||
def check_certificate(domain, ssl_certificate, ssl_private_key, warn_if_expiring_soon=True, rounded_time=False, just_check_domain=False):
|
||||
# Check that the ssl_certificate & ssl_private_key files are good
|
||||
# for the provided domain.
|
||||
|
||||
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
|
||||
from cryptography.x509 import Certificate
|
||||
|
||||
# The ssl_certificate file may contain a chain of certificates. We'll
|
||||
# need to split that up before we can pass anything to openssl or
|
||||
# parse them in Python. Parse it with the cryptography library.
|
||||
try:
|
||||
ssl_cert_chain = load_cert_chain(ssl_certificate)
|
||||
cert = load_pem(ssl_cert_chain[0])
|
||||
if not isinstance(cert, Certificate): raise ValueError("This is not a certificate file.")
|
||||
except ValueError as e:
|
||||
return ("There is a problem with the certificate file: %s" % str(e), None)
|
||||
|
||||
# First check that the domain name is one of the names allowed by
|
||||
# the certificate.
|
||||
if domain is not None:
|
||||
certificate_names, cert_primary_name = get_certificate_domains(cert)
|
||||
|
||||
# Check that the domain appears among the acceptable names, or a wildcard
|
||||
# form of the domain name (which is a stricter check than the specs but
|
||||
# should work in normal cases).
|
||||
wildcard_domain = re.sub("^[^\.]+", "*", domain)
|
||||
if domain not in certificate_names and wildcard_domain not in certificate_names:
|
||||
return ("The certificate is for the wrong domain name. It is for %s."
|
||||
% ", ".join(sorted(certificate_names)), None)
|
||||
|
||||
# Second, check that the certificate matches the private key.
|
||||
if ssl_private_key is not None:
|
||||
try:
|
||||
priv_key = load_pem(open(ssl_private_key, 'rb').read())
|
||||
except ValueError as e:
|
||||
return ("The private key file %s is not a private key file: %s" % (ssl_private_key, str(e)), None)
|
||||
|
||||
if not isinstance(priv_key, RSAPrivateKey):
|
||||
return ("The private key file %s is not a private key file." % ssl_private_key, None)
|
||||
|
||||
if priv_key.public_key().public_numbers() != cert.public_key().public_numbers():
|
||||
return ("The certificate does not correspond to the private key at %s." % ssl_private_key, None)
|
||||
|
||||
# We could also use the openssl command line tool to get the modulus
|
||||
# listed in each file. The output of each command below looks like "Modulus=XXXXX".
|
||||
# $ openssl rsa -inform PEM -noout -modulus -in ssl_private_key
|
||||
# $ openssl x509 -in ssl_certificate -noout -modulus
|
||||
|
||||
# Third, check if the certificate is self-signed. Return a special flag string.
|
||||
if cert.issuer == cert.subject:
|
||||
return ("SELF-SIGNED", None)
|
||||
|
||||
# When selecting which certificate to use for non-primary domains, we check if the primary
|
||||
# certificate or a www-parent-domain certificate is good for the domain. There's no need
|
||||
# to run extra checks beyond this point.
|
||||
if just_check_domain:
|
||||
return ("OK", None)
|
||||
|
||||
# Check that the certificate hasn't expired. The datetimes returned by the
|
||||
# certificate are 'naive' and in UTC. We need to get the current time in UTC.
|
||||
now = datetime.datetime.utcnow()
|
||||
if not(cert.not_valid_before <= now <= cert.not_valid_after):
|
||||
return ("The certificate has expired or is not yet valid. It is valid from %s to %s." % (cert.not_valid_before, cert.not_valid_after), None)
|
||||
|
||||
# Next validate that the certificate is valid. This checks whether the certificate
|
||||
# is self-signed, that the chain of trust makes sense, that it is signed by a CA
|
||||
# that Ubuntu has installed on this machine's list of CAs, and I think that it hasn't
|
||||
# expired.
|
||||
|
||||
# The certificate chain has to be passed separately and is given via STDIN.
|
||||
# This command returns a non-zero exit status in most cases, so trap errors.
|
||||
retcode, verifyoutput = shell('check_output', [
|
||||
"openssl",
|
||||
"verify", "-verbose",
|
||||
"-purpose", "sslserver", "-policy_check",]
|
||||
+ ([] if len(ssl_cert_chain) == 1 else ["-untrusted", "/proc/self/fd/0"])
|
||||
+ [ssl_certificate],
|
||||
input=b"\n\n".join(ssl_cert_chain[1:]),
|
||||
trap=True)
|
||||
|
||||
if "self signed" in verifyoutput:
|
||||
# Certificate is self-signed. Probably we detected this above.
|
||||
return ("SELF-SIGNED", None)
|
||||
|
||||
elif retcode != 0:
|
||||
if "unable to get local issuer certificate" in verifyoutput:
|
||||
return ("The certificate is missing an intermediate chain or the intermediate chain is incorrect or incomplete. (%s)" % verifyoutput, None)
|
||||
|
||||
# There is some unknown problem. Return the `openssl verify` raw output.
|
||||
return ("There is a problem with the SSL certificate.", verifyoutput.strip())
|
||||
|
||||
else:
|
||||
# `openssl verify` returned a zero exit status so the cert is currently
|
||||
# good.
|
||||
|
||||
# But is it expiring soon?
|
||||
cert_expiration_date = cert.not_valid_after
|
||||
ndays = (cert_expiration_date-now).days
|
||||
if not rounded_time or ndays < 7:
|
||||
expiry_info = "The certificate expires in %d days on %s." % (ndays, cert_expiration_date.strftime("%x"))
|
||||
elif ndays <= 14:
|
||||
expiry_info = "The certificate expires in less than two weeks, on %s." % cert_expiration_date.strftime("%x")
|
||||
elif ndays <= 31:
|
||||
expiry_info = "The certificate expires in less than a month, on %s." % cert_expiration_date.strftime("%x")
|
||||
else:
|
||||
expiry_info = "The certificate expires on %s." % cert_expiration_date.strftime("%x")
|
||||
|
||||
if ndays <= 31 and warn_if_expiring_soon:
|
||||
return ("The certificate is expiring soon: " + expiry_info, None)
|
||||
|
||||
# Return the special OK code.
|
||||
return ("OK", expiry_info)
|
||||
|
||||
def load_cert_chain(pemfile):
|
||||
# A certificate .pem file may contain a chain of certificates.
|
||||
# Load the file and split them apart.
|
||||
re_pem = rb"(-+BEGIN (?:.+)-+[\r\n]+(?:[A-Za-z0-9+/=]{1,64}[\r\n]+)+-+END (?:.+)-+[\r\n]+)"
|
||||
with open(pemfile, "rb") as f:
|
||||
pem = f.read() + b"\n" # ensure trailing newline
|
||||
pemblocks = re.findall(re_pem, pem)
|
||||
if len(pemblocks) == 0:
|
||||
raise ValueError("File does not contain valid PEM data.")
|
||||
return pemblocks
|
||||
|
||||
def load_pem(pem):
|
||||
# Parse a "---BEGIN .... END---" PEM string and return a Python object for it
|
||||
# using classes from the cryptography package.
|
||||
from cryptography.x509 import load_pem_x509_certificate
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
pem_type = re.match(b"-+BEGIN (.*?)-+[\r\n]", pem)
|
||||
if pem_type is None:
|
||||
raise ValueError("File is not a valid PEM-formatted file.")
|
||||
pem_type = pem_type.group(1)
|
||||
if pem_type in (b"RSA PRIVATE KEY", b"PRIVATE KEY"):
|
||||
return serialization.load_pem_private_key(pem, password=None, backend=default_backend())
|
||||
if pem_type == b"CERTIFICATE":
|
||||
return load_pem_x509_certificate(pem, default_backend())
|
||||
raise ValueError("Unsupported PEM object type: " + pem_type.decode("ascii", "replace"))
|
||||
|
||||
def get_certificate_domains(cert):
|
||||
from cryptography.x509 import DNSName, ExtensionNotFound, OID_COMMON_NAME, OID_SUBJECT_ALTERNATIVE_NAME
|
||||
import idna
|
||||
|
||||
names = set()
|
||||
cn = None
|
||||
|
||||
# The domain may be found in the Subject Common Name (CN). This comes back as an IDNA (ASCII)
|
||||
# string, which is the format we store domains in - so good.
|
||||
try:
|
||||
cn = cert.subject.get_attributes_for_oid(OID_COMMON_NAME)[0].value
|
||||
names.add(cn)
|
||||
except IndexError:
|
||||
# No common name? Certificate is probably generated incorrectly.
|
||||
# But we'll let it error-out when it doesn't find the domain.
|
||||
pass
|
||||
|
||||
# ... or be one of the Subject Alternative Names. The cryptography library handily IDNA-decodes
|
||||
# the names for us. We must encode back to ASCII, but wildcard certificates can't pass through
|
||||
# IDNA encoding/decoding so we must special-case. See https://github.com/pyca/cryptography/pull/2071.
|
||||
def idna_decode_dns_name(dns_name):
|
||||
if dns_name.startswith("*."):
|
||||
return "*." + idna.encode(dns_name[2:]).decode('ascii')
|
||||
else:
|
||||
return idna.encode(dns_name).decode('ascii')
|
||||
|
||||
try:
|
||||
sans = cert.extensions.get_extension_for_oid(OID_SUBJECT_ALTERNATIVE_NAME).value.get_values_for_type(DNSName)
|
||||
for san in sans:
|
||||
names.add(idna_decode_dns_name(san))
|
||||
except ExtensionNotFound:
|
||||
pass
|
||||
|
||||
return names, cn
|
||||
|
||||
_apt_updates = None
|
||||
def list_apt_updates(apt_update=True):
|
||||
# See if we have this information cached recently.
|
||||
|
||||
@@ -86,7 +86,7 @@
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p style="margin-top: 1.5em"><small>hostmaster@, postmaster@, and admin@ email addresses are required on some domains.</small></p>
|
||||
<p style="margin-top: 1.5em"><small>hostmaster@, postmaster@, admin@ and abuse@ email addresses are required on some domains.</small></p>
|
||||
|
||||
<div style="display: none">
|
||||
<table>
|
||||
|
||||
@@ -82,11 +82,13 @@
|
||||
<button type="submit" class="btn btn-primary">Update</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="secondarydns-clear-instructions" class="form-group" style="display: none">
|
||||
<div class="form-group">
|
||||
<div class="col-sm-offset-1 col-sm-11">
|
||||
<p class="small">
|
||||
Multiple secondary servers can be separated with commas or spaces (i.e., <code>ns2.hostingcompany.com ns3.hostingcompany.com</code>).
|
||||
To enable zone transfers to additional servers without listing them as secondary nameservers, add <code>xfr:IPADDRESS</code>.
|
||||
</p>
|
||||
<p id="secondarydns-clear-instructions" style="display: none" class="small">
|
||||
Clear the input field above and click Update to use this machine itself as secondary DNS, which is the default/normal setup.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<tr><th>Protocol/Method</th> <td>IMAP</td></tr>
|
||||
<tr><th>Mail server</th> <td>{{hostname}}</td>
|
||||
<tr><th>IMAP Port</th> <td>993</td></tr>
|
||||
<tr><th>IMAP Security</th> <td>SSL</td></tr>
|
||||
<tr><th>IMAP Security</th> <td>SSL or TLS</td></tr>
|
||||
<tr><th>SMTP Port</th> <td>587</td></tr>
|
||||
<tr><th>SMTP Security</td> <td>STARTTLS <small>(“always” or “required”, if prompted)</small></td></tr>
|
||||
<tr><th>Username:</th> <td>Your whole email address.</td></tr>
|
||||
@@ -38,6 +38,8 @@
|
||||
|
||||
<p>In addition to setting up your email, you’ll also need to set up <a href="#sync_guide" onclick="return show_panel(this);">contacts and calendar synchronization</a> separately.</p>
|
||||
|
||||
<p>As an alternative to IMAP you can also use the POP protocol: choose POP as the protocol, port 995, and SSL or TLS security in your mail client. The SMTP settings and usernames and passwords remain the same. However, we recommend you use IMAP instead.</p>
|
||||
|
||||
<h4>Exchange/ActiveSync settings</h4>
|
||||
|
||||
<p>On iOS devices, devices on this <a href="http://z-push.org/compatibility/">compatibility list</a>, or using Outlook 2007 or later on Windows 7 and later, you may set up your mail as an Exchange or ActiveSync server. However, we’ve found this to be more buggy than using IMAP as described above. If you encounter any problems, please use the manual settings above.</p>
|
||||
|
||||
@@ -22,12 +22,21 @@
|
||||
|
||||
<h3 id="ssl_install_header">Install SSL Certificate</h3>
|
||||
|
||||
<p>There are many places where you can get a free or cheap SSL certificate. We recommend <a href="https://www.namecheap.com/security/ssl-certificates/domain-validation.aspx">Namecheap’s $9 certificate</a>, <a href="https://www.startssl.com/">StartSSL’s free express lane</a> or <a href="https://buy.wosign.com/free/">Wosign’s free SSL</a></a>.</p>
|
||||
<p>There are many places where you can get a free or cheap SSL certificate. We recommend <a href="https://www.namecheap.com/security/ssl-certificates/domain-validation.aspx">Namecheap’s $9 certificate</a>, <a href="https://www.startssl.com/">StartSSL’s free express lane</a> or <a href="https://buy.wosign.com/free/">WoSign’s free SSL</a></a>.</p>
|
||||
|
||||
<p>Which domain are you getting an SSL certificate for?</p>
|
||||
|
||||
<p><select id="ssldomain" onchange="show_csr()" class="form-control" style="width: auto"></select></p>
|
||||
|
||||
<p>What country are you in? This is required by some SSL certificate providers. You may leave this blank if you know your SSL certificate provider doesn't require it.</p>
|
||||
|
||||
<p><select id="sslcc" onchange="show_csr()" class="form-control" style="width: auto">
|
||||
<option value="">(Select)</option>
|
||||
{% for code, name in csr_country_codes %}
|
||||
<option value="{{code}}">{{name}}</option>
|
||||
{% endfor %}
|
||||
</select></p>
|
||||
|
||||
<div id="csr_info" style="display: none">
|
||||
<p>You will need to provide the SSL certificate provider this Certificate Signing Request (CSR):</p>
|
||||
|
||||
@@ -94,6 +103,7 @@ function show_csr() {
|
||||
"/ssl/csr/" + $('#ssldomain').val(),
|
||||
"POST",
|
||||
{
|
||||
countrycode: $('#sslcc').val()
|
||||
},
|
||||
function(data) {
|
||||
$('#ssl_csr').text(data);
|
||||
|
||||
@@ -32,12 +32,6 @@
|
||||
<p>You MUST manually copy the encryption password from <tt class="backup-encpassword-file"></tt> to a safe and secure location. You will need this file to decrypt backup files. It is NOT stored in your Amazon S3 bucket.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group backup-target-local backup-target-s3">
|
||||
<label for="min-age" class="col-sm-2 control-label">How many days should backups be kept?</label>
|
||||
<div class="col-sm-8">
|
||||
<input type="number" class="form-control" rows="1" id="min-age">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group backup-target-s3">
|
||||
<label for="backup-target-s3-host" class="col-sm-2 control-label">S3 Region</label>
|
||||
<div class="col-sm-8">
|
||||
@@ -66,6 +60,13 @@
|
||||
<input type="text" class="form-control" rows="1" id="backup-target-pass">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group backup-target-local backup-target-s3">
|
||||
<label for="min-age" class="col-sm-2 control-label">Days:</label>
|
||||
<div class="col-sm-8">
|
||||
<input type="number" class="form-control" rows="1" id="min-age">
|
||||
<div class="small" style="margin-top: 2px">This is the <i>minimum</i> number of days backup data is kept for. The box makes an incremental backup, so backup data is often kept much longer. An incremental backup file that is less than this number of days old requires that all previous increments back to the most recent full backup, plus that full backup, remain available.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-offset-2 col-sm-10">
|
||||
<button id="set-s3-backup-button" type="submit" class="btn btn-primary">Save</button>
|
||||
|
||||
@@ -245,11 +245,17 @@ def wait_for_service(port, public, env, timeout):
|
||||
return False
|
||||
time.sleep(min(timeout/4, 1))
|
||||
|
||||
def fix_boto():
|
||||
# Google Compute Engine instances install some Python-2-only boto plugins that
|
||||
# conflict with boto running under Python 3. Disable boto's default configuration
|
||||
# file prior to importing boto so that GCE's plugin is not loaded:
|
||||
import os
|
||||
os.environ["BOTO_CONFIG"] = "/etc/boto3.cfg"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from dns_update import get_dns_domains
|
||||
from web_update import get_web_domains, get_default_www_redirects
|
||||
from web_update import get_web_domains
|
||||
env = load_environment()
|
||||
domains = get_dns_domains(env) | set(get_web_domains(env) + get_default_www_redirects(env))
|
||||
domains = sort_domains(domains, env)
|
||||
domains = get_web_domains(env)
|
||||
for domain in domains:
|
||||
print(domain)
|
||||
|
||||
@@ -2,26 +2,36 @@
|
||||
# domains for which a mail account has been set up.
|
||||
########################################################################
|
||||
|
||||
import os, os.path, shutil, re, tempfile, rtyaml
|
||||
import os.path, re, rtyaml
|
||||
|
||||
from mailconfig import get_mail_domains
|
||||
from dns_update import get_custom_dns_config, do_dns_update, get_dns_zones
|
||||
from dns_update import get_custom_dns_config, get_dns_zones
|
||||
from ssl_certificates import get_ssl_certificates, get_domain_ssl_files, check_certificate
|
||||
from utils import shell, safe_domain_name, sort_domains
|
||||
|
||||
def get_web_domains(env):
|
||||
# What domains should we serve websites for?
|
||||
def get_web_domains(env, include_www_redirects=True):
|
||||
# What domains should we serve HTTP(S) for?
|
||||
domains = set()
|
||||
|
||||
# At the least it's the PRIMARY_HOSTNAME so we can serve webmail
|
||||
# as well as Z-Push for Exchange ActiveSync.
|
||||
domains.add(env['PRIMARY_HOSTNAME'])
|
||||
|
||||
# Also serve web for all mail domains so that we might at least
|
||||
# Serve web for all mail domains so that we might at least
|
||||
# provide auto-discover of email settings, and also a static website
|
||||
# if the user wants to make one. These will require an SSL cert.
|
||||
# if the user wants to make one.
|
||||
domains |= get_mail_domains(env)
|
||||
|
||||
if include_www_redirects:
|
||||
# Add 'www.' subdomains that we want to provide default redirects
|
||||
# to the main domain for. We'll add 'www.' to any DNS zones, i.e.
|
||||
# the topmost of each domain we serve.
|
||||
domains |= set('www.' + zone for zone, zonefile in get_dns_zones(env))
|
||||
|
||||
# ...Unless the domain has an A/AAAA record that maps it to a different
|
||||
# IP address than this box. Remove those domains from our list.
|
||||
domains |= (get_mail_domains(env) - get_domains_with_a_records(env))
|
||||
domains -= get_domains_with_a_records(env)
|
||||
|
||||
# Ensure the PRIMARY_HOSTNAME is in the list so we can serve webmail
|
||||
# as well as Z-Push for Exchange ActiveSync. This can't be removed
|
||||
# by a custom A/AAAA record and is never a 'www.' redirect.
|
||||
domains.add(env['PRIMARY_HOSTNAME'])
|
||||
|
||||
# Sort the list so the nginx conf gets written in a stable order.
|
||||
domains = sort_domains(domains, env)
|
||||
@@ -50,15 +60,6 @@ def get_web_domains_with_root_overrides(env):
|
||||
root_overrides[domain] = (type, value)
|
||||
return root_overrides
|
||||
|
||||
|
||||
def get_default_www_redirects(env):
|
||||
# Returns a list of www subdomains that we want to provide default redirects
|
||||
# for, i.e. any www's that aren't domains the user has actually configured
|
||||
# to serve for real. Which would be unusual.
|
||||
web_domains = set(get_web_domains(env))
|
||||
www_domains = set('www.' + zone for zone, zonefile in get_dns_zones(env))
|
||||
return sort_domains(www_domains - web_domains - get_domains_with_a_records(env), env)
|
||||
|
||||
def do_web_update(env):
|
||||
# Pre-load what SSL certificates we will use for each domain.
|
||||
ssl_certificates = get_ssl_certificates(env)
|
||||
@@ -77,16 +78,20 @@ def do_web_update(env):
|
||||
|
||||
# Add configuration all other web domains.
|
||||
has_root_proxy_or_redirect = get_web_domains_with_root_overrides(env)
|
||||
web_domains_not_redirect = get_web_domains(env, include_www_redirects=False)
|
||||
for domain in get_web_domains(env):
|
||||
if domain == env['PRIMARY_HOSTNAME']: continue # handled above
|
||||
if domain not in has_root_proxy_or_redirect:
|
||||
nginx_conf += make_domain_config(domain, [template0, template1], ssl_certificates, env)
|
||||
if domain == env['PRIMARY_HOSTNAME']:
|
||||
# PRIMARY_HOSTNAME is handled above.
|
||||
continue
|
||||
if domain in web_domains_not_redirect:
|
||||
# This is a regular domain.
|
||||
if domain not in has_root_proxy_or_redirect:
|
||||
nginx_conf += make_domain_config(domain, [template0, template1], ssl_certificates, env)
|
||||
else:
|
||||
nginx_conf += make_domain_config(domain, [template0], ssl_certificates, env)
|
||||
else:
|
||||
nginx_conf += make_domain_config(domain, [template0], ssl_certificates, env)
|
||||
|
||||
# Add default www redirects.
|
||||
for domain in get_default_www_redirects(env):
|
||||
nginx_conf += make_domain_config(domain, [template0, template3], ssl_certificates, env)
|
||||
# Add default 'www.' redirect.
|
||||
nginx_conf += make_domain_config(domain, [template0, template3], ssl_certificates, env)
|
||||
|
||||
# Did the file change? If not, don't bother writing & restarting nginx.
|
||||
nginx_conf_fn = "/etc/nginx/conf.d/local.conf"
|
||||
@@ -185,214 +190,12 @@ def get_web_root(domain, env, test_exists=True):
|
||||
if os.path.exists(root) or not test_exists: break
|
||||
return root
|
||||
|
||||
def get_ssl_certificates(env):
|
||||
# Scan all of the installed SSL certificates and map every domain
|
||||
# that the certificates are good for to the best certificate for
|
||||
# the domain.
|
||||
|
||||
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
|
||||
from cryptography.x509 import Certificate
|
||||
|
||||
# The certificates are all stored here:
|
||||
ssl_root = os.path.join(env["STORAGE_ROOT"], 'ssl')
|
||||
|
||||
# List all of the files in the SSL directory and one level deep.
|
||||
def get_file_list():
|
||||
for fn in os.listdir(ssl_root):
|
||||
fn = os.path.join(ssl_root, fn)
|
||||
if os.path.isfile(fn):
|
||||
yield fn
|
||||
elif os.path.isdir(fn):
|
||||
for fn1 in os.listdir(fn):
|
||||
fn1 = os.path.join(fn, fn1)
|
||||
if os.path.isfile(fn1):
|
||||
yield fn1
|
||||
|
||||
# Remember stuff.
|
||||
private_keys = { }
|
||||
certificates = [ ]
|
||||
|
||||
# Scan each of the files to find private keys and certificates.
|
||||
# We must load all of the private keys first before processing
|
||||
# certificates so that we can check that we have a private key
|
||||
# available before using a certificate.
|
||||
from status_checks import load_cert_chain, load_pem
|
||||
for fn in get_file_list():
|
||||
try:
|
||||
pem = load_pem(load_cert_chain(fn)[0])
|
||||
except ValueError:
|
||||
# Not a valid PEM format for a PEM type we care about.
|
||||
continue
|
||||
|
||||
# Remember where we got this object.
|
||||
pem._filename = fn
|
||||
|
||||
# Is it a private key?
|
||||
if isinstance(pem, RSAPrivateKey):
|
||||
private_keys[pem.public_key().public_numbers()] = pem
|
||||
|
||||
# Is it a certificate?
|
||||
if isinstance(pem, Certificate):
|
||||
certificates.append(pem)
|
||||
|
||||
# Process the certificates.
|
||||
domains = { }
|
||||
from status_checks import get_certificate_domains
|
||||
for cert in certificates:
|
||||
# What domains is this certificate good for?
|
||||
cert_domains, primary_domain = get_certificate_domains(cert)
|
||||
cert._primary_domain = primary_domain
|
||||
|
||||
# Is there a private key file for this certificate?
|
||||
private_key = private_keys.get(cert.public_key().public_numbers())
|
||||
if not private_key:
|
||||
continue
|
||||
cert._private_key = private_key
|
||||
|
||||
# Add this cert to the list of certs usable for the domains.
|
||||
for domain in cert_domains:
|
||||
domains.setdefault(domain, []).append(cert)
|
||||
|
||||
# Sort the certificates to prefer good ones.
|
||||
import datetime
|
||||
now = datetime.datetime.utcnow()
|
||||
ret = { }
|
||||
for domain, cert_list in domains.items():
|
||||
cert_list.sort(key = lambda cert : (
|
||||
# must be valid NOW
|
||||
cert.not_valid_before <= now <= cert.not_valid_after,
|
||||
|
||||
# prefer one that is not self-signed
|
||||
cert.issuer != cert.subject,
|
||||
|
||||
# prefer one with the expiration furthest into the future so
|
||||
# that we can easily rotate to new certs as we get them
|
||||
cert.not_valid_after,
|
||||
|
||||
# in case a certificate is installed in multiple paths,
|
||||
# prefer the... lexicographically last one?
|
||||
cert._filename,
|
||||
|
||||
), reverse=True)
|
||||
cert = cert_list.pop(0)
|
||||
ret[domain] = {
|
||||
"private-key": cert._private_key._filename,
|
||||
"certificate": cert._filename,
|
||||
"primary-domain": cert._primary_domain,
|
||||
}
|
||||
|
||||
return ret
|
||||
|
||||
def get_domain_ssl_files(domain, ssl_certificates, env, allow_missing_cert=False):
|
||||
# Get the default paths.
|
||||
ssl_private_key = os.path.join(os.path.join(env["STORAGE_ROOT"], 'ssl', 'ssl_private_key.pem'))
|
||||
ssl_certificate = os.path.join(os.path.join(env["STORAGE_ROOT"], 'ssl', 'ssl_certificate.pem'))
|
||||
|
||||
if domain == env['PRIMARY_HOSTNAME']:
|
||||
# The primary domain must use the server certificate because
|
||||
# it is hard-coded in some service configuration files.
|
||||
return ssl_private_key, ssl_certificate, None
|
||||
|
||||
wildcard_domain = re.sub("^[^\.]+", "*", domain)
|
||||
|
||||
if domain in ssl_certificates:
|
||||
cert_info = ssl_certificates[domain]
|
||||
cert_type = "multi-domain"
|
||||
elif wildcard_domain in ssl_certificates:
|
||||
cert_info = ssl_certificates[wildcard_domain]
|
||||
cert_type = "wildcard"
|
||||
elif not allow_missing_cert:
|
||||
# No certificate is available for this domain! Return default files.
|
||||
ssl_via = "Using certificate for %s." % env['PRIMARY_HOSTNAME']
|
||||
return ssl_private_key, ssl_certificate, ssl_via
|
||||
else:
|
||||
# No certificate is available - and warn appropriately.
|
||||
return None
|
||||
|
||||
# 'via' is a hint to the user about which certificate is in use for the domain
|
||||
if cert_info['certificate'] == os.path.join(env["STORAGE_ROOT"], 'ssl', 'ssl_certificate.pem'):
|
||||
# Using the server certificate.
|
||||
via = "Using same %s certificate as for %s." % (cert_type, env['PRIMARY_HOSTNAME'])
|
||||
elif cert_info['primary-domain'] != domain and cert_info['primary-domain'] in ssl_certificates and cert_info == ssl_certificates[cert_info['primary-domain']]:
|
||||
via = "Using same %s certificate as for %s." % (cert_type, cert_info['primary-domain'])
|
||||
else:
|
||||
via = None # don't show a hint - show expiration info instead
|
||||
|
||||
return cert_info['private-key'], cert_info['certificate'], via
|
||||
|
||||
def create_csr(domain, ssl_key, env):
|
||||
return shell("check_output", [
|
||||
"openssl", "req", "-new",
|
||||
"-key", ssl_key,
|
||||
"-sha256",
|
||||
"-subj", "/C=%s/ST=/L=/O=/CN=%s" % (env["CSR_COUNTRY"], domain)])
|
||||
|
||||
def install_cert(domain, ssl_cert, ssl_chain, env):
|
||||
if domain not in get_web_domains(env) + get_default_www_redirects(env):
|
||||
return "Invalid domain name."
|
||||
|
||||
# Write the combined cert+chain to a temporary path and validate that it is OK.
|
||||
# The certificate always goes above the chain.
|
||||
import tempfile, os
|
||||
fd, fn = tempfile.mkstemp('.pem')
|
||||
os.write(fd, (ssl_cert + '\n' + ssl_chain).encode("ascii"))
|
||||
os.close(fd)
|
||||
|
||||
# Do validation on the certificate before installing it.
|
||||
from status_checks import check_certificate
|
||||
ssl_private_key = os.path.join(os.path.join(env["STORAGE_ROOT"], 'ssl', 'ssl_private_key.pem'))
|
||||
cert_status, cert_status_details = check_certificate(domain, fn, ssl_private_key)
|
||||
if cert_status != "OK":
|
||||
if cert_status == "SELF-SIGNED":
|
||||
cert_status = "This is a self-signed certificate. I can't install that."
|
||||
os.unlink(fn)
|
||||
if cert_status_details is not None:
|
||||
cert_status += " " + cert_status_details
|
||||
return cert_status
|
||||
|
||||
# Where to put it?
|
||||
if domain == env['PRIMARY_HOSTNAME']:
|
||||
ssl_certificate = os.path.join(os.path.join(env["STORAGE_ROOT"], 'ssl', 'ssl_certificate.pem'))
|
||||
else:
|
||||
# Make a unique path for the certificate.
|
||||
from status_checks import load_cert_chain, load_pem, get_certificate_domains
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from binascii import hexlify
|
||||
cert = load_pem(load_cert_chain(fn)[0])
|
||||
all_domains, cn = get_certificate_domains(cert)
|
||||
path = "%s-%s-%s" % (
|
||||
cn, # common name
|
||||
cert.not_valid_after.date().isoformat().replace("-", ""), # expiration date
|
||||
hexlify(cert.fingerprint(hashes.SHA256())).decode("ascii")[0:8], # fingerprint prefix
|
||||
)
|
||||
ssl_certificate = os.path.join(os.path.join(env["STORAGE_ROOT"], 'ssl', path, 'ssl_certificate.pem'))
|
||||
|
||||
# Install the certificate.
|
||||
os.makedirs(os.path.dirname(ssl_certificate), exist_ok=True)
|
||||
shutil.move(fn, ssl_certificate)
|
||||
|
||||
ret = ["OK"]
|
||||
|
||||
# When updating the cert for PRIMARY_HOSTNAME, also update DNS because it is
|
||||
# used in the DANE TLSA record and restart postfix and dovecot which use
|
||||
# that certificate.
|
||||
if domain == env['PRIMARY_HOSTNAME']:
|
||||
ret.append( do_dns_update(env) )
|
||||
|
||||
shell('check_call', ["/usr/sbin/service", "postfix", "restart"])
|
||||
shell('check_call', ["/usr/sbin/service", "dovecot", "restart"])
|
||||
ret.append("mail services restarted")
|
||||
|
||||
# Kick nginx so it sees the cert.
|
||||
ret.append( do_web_update(env) )
|
||||
return "\n".join(ret)
|
||||
|
||||
def get_web_domains_info(env):
|
||||
has_root_proxy_or_redirect = get_web_domains_with_root_overrides(env)
|
||||
www_redirects = set(get_web_domains(env)) - set(get_web_domains(env, include_www_redirects=False))
|
||||
has_root_proxy_or_redirect = set(get_web_domains_with_root_overrides(env))
|
||||
|
||||
# for the SSL config panel, get cert status
|
||||
def check_cert(domain):
|
||||
from status_checks import check_certificate
|
||||
ssl_certificates = get_ssl_certificates(env)
|
||||
x = get_domain_ssl_files(domain, ssl_certificates, env, allow_missing_cert=True)
|
||||
if x is None: return ("danger", "No Certificate Installed")
|
||||
@@ -415,15 +218,7 @@ def get_web_domains_info(env):
|
||||
"root": get_web_root(domain, env),
|
||||
"custom_root": get_web_root(domain, env, test_exists=False),
|
||||
"ssl_certificate": check_cert(domain),
|
||||
"static_enabled": domain not in has_root_proxy_or_redirect,
|
||||
"static_enabled": domain not in (www_redirects | has_root_proxy_or_redirect),
|
||||
}
|
||||
for domain in get_web_domains(env)
|
||||
] + \
|
||||
[
|
||||
{
|
||||
"domain": domain,
|
||||
"ssl_certificate": check_cert(domain),
|
||||
"static_enabled": False,
|
||||
}
|
||||
for domain in get_default_www_redirects(env)
|
||||
]
|
||||
]
|
||||
@@ -7,7 +7,7 @@
|
||||
#########################################################
|
||||
|
||||
if [ -z "$TAG" ]; then
|
||||
TAG=v0.14
|
||||
TAG=v0.15
|
||||
fi
|
||||
|
||||
# Are we running as root?
|
||||
|
||||
10
setup/dns.sh
10
setup/dns.sh
@@ -78,7 +78,7 @@ FIRST=1 #NODOC
|
||||
for algo in RSASHA1-NSEC3-SHA1 RSASHA256; do
|
||||
if [ ! -f "$STORAGE_ROOT/dns/dnssec/$algo.conf" ]; then
|
||||
if [ $FIRST == 1 ]; then
|
||||
echo "Generating DNSSEC signing keys. This may take a few minutes..."
|
||||
echo "Generating DNSSEC signing keys..."
|
||||
FIRST=0 #NODOC
|
||||
fi
|
||||
|
||||
@@ -88,13 +88,17 @@ if [ ! -f "$STORAGE_ROOT/dns/dnssec/$algo.conf" ]; then
|
||||
#
|
||||
# `ldns-keygen` outputs the new key's filename to stdout, which
|
||||
# we're capturing into the `KSK` variable.
|
||||
KSK=$(umask 077; cd $STORAGE_ROOT/dns/dnssec; ldns-keygen -a $algo -b 2048 -k _domain_);
|
||||
#
|
||||
# ldns-keygen uses /dev/random for generating random numbers by default.
|
||||
# This is slow and unecessary if we ensure /dev/urandom is seeded properly,
|
||||
# so we use /dev/urandom. See system.sh for an explanation. See #596, #115.
|
||||
KSK=$(umask 077; cd $STORAGE_ROOT/dns/dnssec; ldns-keygen -r /dev/urandom -a $algo -b 2048 -k _domain_);
|
||||
|
||||
# Now create a Zone-Signing Key (ZSK) which is expected to be
|
||||
# rotated more often than a KSK, although we have no plans to
|
||||
# rotate it (and doing so would be difficult to do without
|
||||
# disturbing DNS availability.) Omit `-k` and use a shorter key length.
|
||||
ZSK=$(umask 077; cd $STORAGE_ROOT/dns/dnssec; ldns-keygen -a $algo -b 1024 _domain_);
|
||||
ZSK=$(umask 077; cd $STORAGE_ROOT/dns/dnssec; ldns-keygen -r /dev/urandom -a $algo -b 1024 _domain_);
|
||||
|
||||
# These generate two sets of files like:
|
||||
#
|
||||
|
||||
@@ -88,18 +88,19 @@ sed -i "s/#port = 110/port = 0/" /etc/dovecot/conf.d/10-master.conf
|
||||
# this are minimal. But for good measure, let's go to 4 minutes to halve the
|
||||
# bandwidth and number of times the device's networking might be woken up.
|
||||
# The risk is that if the connection is silent for too long it might be reset
|
||||
# by a peer. See #129 and http://razor.occams.info/blog/2014/08/09/how-bad-is-imap-idle/.
|
||||
# by a peer. See [#129](https://github.com/mail-in-a-box/mailinabox/issues/129)
|
||||
# and [How bad is IMAP IDLE](http://razor.occams.info/blog/2014/08/09/how-bad-is-imap-idle/).
|
||||
tools/editconf.py /etc/dovecot/conf.d/20-imap.conf \
|
||||
imap_idle_notify_interval="4 mins"
|
||||
|
||||
# Set POP3 UIDL
|
||||
# UIDLs are used by POP3 clients to keep track of what messages they've downloaded.
|
||||
# Set POP3 UIDL.
|
||||
# UIDLs are used by POP3 clients to keep track of what messages they've downloaded.
|
||||
# For new POP3 servers, the easiest way to set up UIDLs is to use IMAP's UIDVALIDITY
|
||||
# and UID values, the default in Dovecot.
|
||||
tools/editconf.py /etc/dovecot/conf.d/20-pop3.conf \
|
||||
pop3_uidl_format="%08Xu%08Xv"
|
||||
|
||||
# Full Text Search - Enable full text search of mail using dovecot's lucene plugin,
|
||||
# Full Text Search - Enable full text search of mail using dovecot's lucene plugin,
|
||||
# which *we* package and distribute (dovecot-lucene package).
|
||||
tools/editconf.py /etc/dovecot/conf.d/10-mail.conf \
|
||||
mail_plugins="\$mail_plugins fts fts_lucene"
|
||||
|
||||
@@ -4,13 +4,14 @@ source setup/functions.sh
|
||||
|
||||
echo "Installing Mail-in-a-Box system management daemon..."
|
||||
|
||||
# build-essential libssl-dev libffi-dev python3-dev: Required to pip install cryptography.
|
||||
apt_install python3-flask links duplicity libyaml-dev python3-dnspython python3-dateutil \
|
||||
build-essential libssl-dev libffi-dev python3-dev python-pip
|
||||
hide_output pip3 install --upgrade rtyaml email_validator>=1.0.0 idna>=2.0.0 cryptography>=1.0.2 boto
|
||||
# Switching python 2 boto to package manager's, not pypi's.
|
||||
if [ -f /usr/local/lib/python2.7/dist-packages/boto/__init__.py ]; then hide_output pip uninstall -y boto; fi
|
||||
|
||||
# duplicity uses python 2 so we need to use the python 2 package of boto
|
||||
hide_output pip install --upgrade boto
|
||||
# build-essential libssl-dev libffi-dev python3-dev: Required to pip install cryptography.
|
||||
apt_install python3-flask links duplicity python-boto libyaml-dev python3-dnspython python3-dateutil \
|
||||
build-essential libssl-dev libffi-dev python3-dev python-pip
|
||||
hide_output pip3 install --upgrade rtyaml "email_validator>=1.0.0" "idna>=2.0.0" "cryptography>=1.0.2" boto
|
||||
|
||||
# email_validator is repeated in setup/questions.sh
|
||||
|
||||
@@ -30,25 +31,18 @@ rm -f /etc/init.d/mailinabox
|
||||
ln -s $(pwd)/conf/management-initscript /etc/init.d/mailinabox
|
||||
hide_output update-rc.d mailinabox defaults
|
||||
|
||||
# Perform a daily backup.
|
||||
cat > /etc/cron.daily/mailinabox-backup << EOF;
|
||||
#!/bin/bash
|
||||
# Mail-in-a-Box --- Do not edit / will be overwritten on update.
|
||||
# Perform a backup.
|
||||
$(pwd)/management/backup.py
|
||||
EOF
|
||||
chmod +x /etc/cron.daily/mailinabox-backup
|
||||
# Remove old files we no longer use.
|
||||
rm -f /etc/cron.daily/mailinabox-backup
|
||||
rm -f /etc/cron.daily/mailinabox-statuschecks
|
||||
|
||||
# Perform daily status checks. Compare each day to the previous
|
||||
# for changes and mail the changes to the administrator.
|
||||
cat > /etc/cron.daily/mailinabox-statuschecks << EOF;
|
||||
#!/bin/bash
|
||||
# Mail-in-a-Box --- Do not edit / will be overwritten on update.
|
||||
# Run status checks.
|
||||
$(pwd)/management/status_checks.py --show-changes --smtp
|
||||
EOF
|
||||
chmod +x /etc/cron.daily/mailinabox-statuschecks
|
||||
# Perform nightly tasks at 3am in system time: take a backup, run
|
||||
# status checks and email the administrator any changes.
|
||||
|
||||
cat > /etc/cron.d/mailinabox-nightly << EOF;
|
||||
# Mail-in-a-Box --- Do not edit / will be overwritten on update.
|
||||
# Run nightly tasks: backup, status checks.
|
||||
0 3 * * * root (cd `pwd` && management/daily_tasks.sh)
|
||||
EOF
|
||||
|
||||
# Start it.
|
||||
restart_service mailinabox
|
||||
|
||||
@@ -111,6 +111,32 @@ def migration_9(env):
|
||||
db = os.path.join(env["STORAGE_ROOT"], 'mail/users.sqlite')
|
||||
shell("check_call", ["sqlite3", db, "ALTER TABLE aliases ADD permitted_senders TEXT"])
|
||||
|
||||
def migration_10(env):
|
||||
# Clean up the SSL certificates directory.
|
||||
|
||||
# Move the primary certificate to a new name and then
|
||||
# symlink it to the system certificate path.
|
||||
import datetime
|
||||
system_certificate = os.path.join(env["STORAGE_ROOT"], 'ssl/ssl_certificate.pem')
|
||||
if not os.path.islink(system_certificate): # not already a symlink
|
||||
new_path = os.path.join(env["STORAGE_ROOT"], 'ssl', env['PRIMARY_HOSTNAME'] + "-" + datetime.datetime.now().date().isoformat().replace("-", "") + ".pem")
|
||||
print("Renamed", system_certificate, "to", new_path, "and created a symlink for the original location.")
|
||||
shutil.move(system_certificate, new_path)
|
||||
os.symlink(new_path, system_certificate)
|
||||
|
||||
# Flatten the directory structure. For any directory
|
||||
# that contains a single file named ssl_certificate.pem,
|
||||
# move the file out and name it the same as the directory,
|
||||
# and remove the directory.
|
||||
for sslcert in glob.glob(os.path.join( env["STORAGE_ROOT"], 'ssl/*/ssl_certificate.pem' )):
|
||||
d = os.path.dirname(sslcert)
|
||||
if len(os.listdir(d)) == 1:
|
||||
# This certificate is the only file in that directory.
|
||||
newname = os.path.join(env["STORAGE_ROOT"], 'ssl', os.path.basename(d) + '.pem')
|
||||
if not os.path.exists(newname):
|
||||
shutil.move(sslcert, newname)
|
||||
os.rmdir(d)
|
||||
|
||||
def get_current_migration():
|
||||
ver = 0
|
||||
while True:
|
||||
|
||||
@@ -133,7 +133,7 @@ EOF
|
||||
# Create an auto-configuration file to fill in database settings
|
||||
# when the install script is run. Make an administrator account
|
||||
# here or else the install can't finish.
|
||||
adminpassword=$(dd if=/dev/random bs=1 count=40 2>/dev/null | sha1sum | fold -w 30 | head -n 1)
|
||||
adminpassword=$(dd if=/dev/urandom bs=1 count=40 2>/dev/null | sha1sum | fold -w 30 | head -n 1)
|
||||
cat > /usr/local/lib/owncloud/config/autoconfig.php <<EOF;
|
||||
<?php
|
||||
\$AUTOCONFIG = array (
|
||||
@@ -173,6 +173,7 @@ include("$STORAGE_ROOT/owncloud/config.php");
|
||||
|
||||
\$CONFIG['memcache.local'] = '\\OC\\Memcache\\Memcached';
|
||||
\$CONFIG['overwrite.cli.url'] = '/cloud';
|
||||
\$CONFIG['mail_from_address'] = 'administrator'; # just the local part, matches our master administrator address
|
||||
|
||||
echo "<?php\n\\\$CONFIG = ";
|
||||
var_export(\$CONFIG);
|
||||
|
||||
@@ -13,7 +13,7 @@ if [ -z "$NONINTERACTIVE" ]; then
|
||||
fi
|
||||
|
||||
# email_validator is repeated in setup/management.sh
|
||||
hide_output pip3 install email_validator==1.0.0 || exit 1
|
||||
hide_output pip3 install "email_validator>=1.0.0" || exit 1
|
||||
|
||||
message_box "Mail-in-a-Box Installation" \
|
||||
"Hello and thanks for deploying a Mail-in-a-Box!
|
||||
@@ -168,35 +168,6 @@ if [[ -z "$PRIVATE_IP" && -z "$PRIVATE_IPV6" ]]; then
|
||||
exit
|
||||
fi
|
||||
|
||||
# We need a country code to generate a certificate signing request. However
|
||||
# if a CSR already exists then we won't be generating a new one and there's
|
||||
# no reason to ask for the country code now. $STORAGE_ROOT has not yet been
|
||||
# set so we'll check if $DEFAULT_STORAGE_ROOT and $DEFAULT_CSR_COUNTRY are
|
||||
# set (the values from the current mailinabox.conf) and if the CSR exists
|
||||
# in the expected location.
|
||||
if [ ! -z "$DEFAULT_STORAGE_ROOT" ] && [ ! -z "$DEFAULT_CSR_COUNTRY" ] && [ -f $DEFAULT_STORAGE_ROOT/ssl/ssl_cert_sign_req.csr ]; then
|
||||
CSR_COUNTRY=$DEFAULT_CSR_COUNTRY
|
||||
fi
|
||||
|
||||
if [ -z "$CSR_COUNTRY" ]; then
|
||||
# Get a list of country codes. Separate codes from country names with a ^.
|
||||
# The input_menu function modifies shell word expansion to ignore spaces
|
||||
# (since country names can have spaces) and use ^ instead.
|
||||
country_code_list=$(grep -v "^#" setup/csr_country_codes.tsv | sed "s/\(..\)\t\([^\t]*\).*/\1^\2/")
|
||||
|
||||
input_menu "Country Code" \
|
||||
"Choose the country where you live or where your organization is based.
|
||||
\n\n(This is used to create an SSL certificate.)
|
||||
\n\nCountry Code:" \
|
||||
"$country_code_list" \
|
||||
CSR_COUNTRY
|
||||
|
||||
if [ -z "$CSR_COUNTRY" ]; then
|
||||
# user hit ESC/cancel
|
||||
exit
|
||||
fi
|
||||
fi
|
||||
|
||||
# Automatic configuration, e.g. as used in our Vagrant configuration.
|
||||
if [ "$PUBLIC_IP" = "auto" ]; then
|
||||
# Use a public API to get our public IP address, or fall back to local network configuration.
|
||||
@@ -207,8 +178,6 @@ if [ "$PUBLIC_IPV6" = "auto" ]; then
|
||||
PUBLIC_IPV6=$(get_publicip_from_web_service 6 || get_default_privateip 6)
|
||||
fi
|
||||
if [ "$PRIMARY_HOSTNAME" = "auto" ]; then
|
||||
# Use reverse DNS to get this machine's hostname. Install bind9-host early.
|
||||
hide_output apt-get -y install bind9-host
|
||||
PRIMARY_HOSTNAME=$(get_default_hostname)
|
||||
elif [ "$PRIMARY_HOSTNAME" = "auto-easy" ]; then
|
||||
# Generate a probably-unique subdomain under our justtesting.email domain.
|
||||
|
||||
95
setup/ssl.sh
95
setup/ssl.sh
@@ -1,53 +1,98 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# SSL Certificate
|
||||
# ---------------
|
||||
# RSA private key, SSL certificate, Diffie-Hellman bits files
|
||||
# -------------------------------------------
|
||||
|
||||
# Create a self-signed SSL certificate if one has not yet been created.
|
||||
# Create an RSA private key, a self-signed SSL certificate, and some
|
||||
# Diffie-Hellman cipher bits, if they have not yet been created.
|
||||
#
|
||||
# The certificate is for PRIMARY_HOSTNAME specifically and is used for:
|
||||
# The RSA private key and certificate are used for:
|
||||
#
|
||||
# * DNSSEC DANE TLSA records
|
||||
# * IMAP
|
||||
# * SMTP submission (port 587) and opportunistic TLS (when on the receiving end)
|
||||
# * the DNSSEC DANE TLSA record for SMTP
|
||||
# * HTTPS (for PRIMARY_HOSTNAME only)
|
||||
# * SMTP (opportunistic TLS for port 25 and submission on port 587)
|
||||
# * HTTPS
|
||||
#
|
||||
# When other domains besides PRIMARY_HOSTNAME are served over HTTPS,
|
||||
# we generate a domain-specific self-signed certificate in the management
|
||||
# daemon (web_update.py) as needed.
|
||||
# The certificate is created with its CN set to the PRIMARY_HOSTNAME. It is
|
||||
# also used for other domains served over HTTPS until the user installs a
|
||||
# better certificate for those domains.
|
||||
#
|
||||
# The Diffie-Hellman cipher bits are used for SMTP and HTTPS, when a
|
||||
# Diffie-Hellman cipher is selected during TLS negotiation. Diffie-Hellman
|
||||
# provides Perfect Forward Secrecy.
|
||||
|
||||
source setup/functions.sh # load our functions
|
||||
source /etc/mailinabox.conf # load global vars
|
||||
|
||||
echo "Creating initial SSL certificate and perfect forward secrecy Diffie-Hellman parameters..."
|
||||
# Show a status line if we are going to take any action in this file.
|
||||
if [ ! -f /usr/bin/openssl ] \
|
||||
|| [ ! -f $STORAGE_ROOT/ssl/ssl_private_key.pem ] \
|
||||
|| [ ! -f $STORAGE_ROOT/ssl/ssl_certificate.pem ] \
|
||||
|| [ ! -f $STORAGE_ROOT/ssl/dh2048.pem ]; then
|
||||
echo "Creating initial SSL certificate and perfect forward secrecy Diffie-Hellman parameters..."
|
||||
fi
|
||||
|
||||
# Install openssl.
|
||||
|
||||
apt_install openssl
|
||||
|
||||
# Create a directory to store TLS-related things like "SSL" certificates.
|
||||
|
||||
mkdir -p $STORAGE_ROOT/ssl
|
||||
|
||||
# Generate a new private key.
|
||||
# Set the umask so the key file is not world-readable.
|
||||
#
|
||||
# The key is only as good as the entropy available to openssl so that it
|
||||
# can generate a random key. "OpenSSL’s built-in RSA key generator ....
|
||||
# is seeded on first use with (on Linux) 32 bytes read from /dev/urandom,
|
||||
# the process ID, user ID, and the current time in seconds. [During key
|
||||
# generation OpenSSL] mixes into the entropy pool the current time in seconds,
|
||||
# the process ID, and the possibly uninitialized contents of a ... buffer
|
||||
# ... dozens to hundreds of times."
|
||||
#
|
||||
# A perfect storm of issues can cause the generated key to be not very random:
|
||||
#
|
||||
# * improperly seeded /dev/urandom, but see system.sh for how we mitigate this
|
||||
# * the user ID of this process is always the same (we're root), so that seed is useless
|
||||
# * zero'd memory (plausible on embedded systems, cloud VMs?)
|
||||
# * a predictable process ID (likely on an embedded/virtualized system)
|
||||
# * a system clock reset to a fixed time on boot
|
||||
#
|
||||
# Since we properly seed /dev/urandom in system.sh we should be fine, but I leave
|
||||
# in the rest of the notes in case that ever changes.
|
||||
if [ ! -f $STORAGE_ROOT/ssl/ssl_private_key.pem ]; then
|
||||
# Set the umask so the key file is never world-readable.
|
||||
(umask 077; hide_output \
|
||||
openssl genrsa -out $STORAGE_ROOT/ssl/ssl_private_key.pem 2048)
|
||||
fi
|
||||
|
||||
# Generate a certificate signing request.
|
||||
if [ ! -f $STORAGE_ROOT/ssl/ssl_cert_sign_req.csr ]; then
|
||||
hide_output \
|
||||
openssl req -new -key $STORAGE_ROOT/ssl/ssl_private_key.pem -out $STORAGE_ROOT/ssl/ssl_cert_sign_req.csr \
|
||||
-sha256 -subj "/C=$CSR_COUNTRY/ST=/L=/O=/CN=$PRIMARY_HOSTNAME"
|
||||
fi
|
||||
|
||||
# Generate a SSL certificate by self-signing.
|
||||
# Generate a self-signed SSL certificate because things like nginx, dovecot,
|
||||
# etc. won't even start without some certificate in place, and we need nginx
|
||||
# so we can offer the user a control panel to install a better certificate.
|
||||
if [ ! -f $STORAGE_ROOT/ssl/ssl_certificate.pem ]; then
|
||||
# Generate a certificate signing request.
|
||||
CSR=/tmp/ssl_cert_sign_req-$$.csr
|
||||
hide_output \
|
||||
openssl req -new -key $STORAGE_ROOT/ssl/ssl_private_key.pem -out $CSR \
|
||||
-sha256 -subj "/C=/ST=/L=/O=/CN=$PRIMARY_HOSTNAME"
|
||||
|
||||
# Generate the self-signed certificate.
|
||||
CERT=$STORAGE_ROOT/ssl/$PRIMARY_HOSTNAME-selfsigned-$(date --rfc-3339=date | sed s/-//g).pem
|
||||
hide_output \
|
||||
openssl x509 -req -days 365 \
|
||||
-in $STORAGE_ROOT/ssl/ssl_cert_sign_req.csr -signkey $STORAGE_ROOT/ssl/ssl_private_key.pem -out $STORAGE_ROOT/ssl/ssl_certificate.pem
|
||||
-in $CSR -signkey $STORAGE_ROOT/ssl/ssl_private_key.pem -out $CERT
|
||||
|
||||
# Delete the certificate signing request because it has no other purpose.
|
||||
rm -f $CSR
|
||||
|
||||
# Symlink the certificate into the system certificate path, so system services
|
||||
# can find it.
|
||||
ln -s $CERT $STORAGE_ROOT/ssl/ssl_certificate.pem
|
||||
fi
|
||||
|
||||
# For nginx and postfix, pre-generate some Diffie-Hellman cipher bits which is
|
||||
# used when a Diffie-Hellman cipher is selected during TLS negotiation. Diffie-Hellman
|
||||
# provides Perfect Forward Secrecy. openssl's default is 1024 bits, but we'll
|
||||
# create 2048.
|
||||
# Generate some Diffie-Hellman cipher bits.
|
||||
# openssl's default bit length for this is 1024 bits, but we'll create
|
||||
# 2048 bits of bits per the latest recommendations.
|
||||
if [ ! -f $STORAGE_ROOT/ssl/dh2048.pem ]; then
|
||||
openssl dhparam -out $STORAGE_ROOT/ssl/dh2048.pem 2048
|
||||
fi
|
||||
|
||||
@@ -34,6 +34,8 @@ if [ -f /etc/mailinabox.conf ]; then
|
||||
cat /etc/mailinabox.conf | sed s/^/DEFAULT_/ > /tmp/mailinabox.prev.conf
|
||||
source /tmp/mailinabox.prev.conf
|
||||
rm -f /tmp/mailinabox.prev.conf
|
||||
else
|
||||
FIRST_TIME_SETUP=1
|
||||
fi
|
||||
|
||||
# Put a start script in a global location. We tell the user to run 'mailinabox'
|
||||
@@ -45,7 +47,7 @@ source setup/start.sh
|
||||
EOF
|
||||
chmod +x /usr/local/bin/mailinabox
|
||||
|
||||
# Ask the user for the PRIMARY_HOSTNAME, PUBLIC_IP, PUBLIC_IPV6, and CSR_COUNTRY
|
||||
# Ask the user for the PRIMARY_HOSTNAME, PUBLIC_IP, and PUBLIC_IPV6,
|
||||
# if values have not already been set in environment variables. When running
|
||||
# non-interactively, be sure to set values for all! Also sets STORAGE_USER and
|
||||
# STORAGE_ROOT.
|
||||
@@ -87,7 +89,6 @@ PUBLIC_IP=$PUBLIC_IP
|
||||
PUBLIC_IPV6=$PUBLIC_IPV6
|
||||
PRIVATE_IP=$PRIVATE_IP
|
||||
PRIVATE_IPV6=$PRIVATE_IPV6
|
||||
CSR_COUNTRY=$CSR_COUNTRY
|
||||
EOF
|
||||
|
||||
# Start service configuration.
|
||||
|
||||
108
setup/system.sh
108
setup/system.sh
@@ -1,3 +1,4 @@
|
||||
source /etc/mailinabox.conf
|
||||
source setup/functions.sh # load our functions
|
||||
|
||||
# Basic System Configuration
|
||||
@@ -11,12 +12,9 @@ source setup/functions.sh # load our functions
|
||||
# text search plugin for (and by) dovecot, which is not available in
|
||||
# Ubuntu currently.
|
||||
#
|
||||
# Add that to the system's list of repositories using add-apt-repository.
|
||||
# But add-apt-repository may not be installed. If it's not available,
|
||||
# then install it. But we have to run apt-get update before we try to
|
||||
# install anything so the package index is up to date. After adding the
|
||||
# PPA, we have to run apt-get update *again* to load the PPA's index,
|
||||
# so this must precede the apt-get update line below.
|
||||
# So, first ensure add-apt-repository is installed, then use it to install
|
||||
# the [mail-in-a-box ppa](https://launchpad.net/~mail-in-a-box/+archive/ubuntu/ppa).
|
||||
|
||||
|
||||
if [ ! -f /usr/bin/add-apt-repository ]; then
|
||||
echo "Installing add-apt-repository..."
|
||||
@@ -54,8 +52,100 @@ apt_get_quiet upgrade
|
||||
echo Installing system packages...
|
||||
apt_install python3 python3-dev python3-pip \
|
||||
netcat-openbsd wget curl git sudo coreutils bc \
|
||||
haveged unattended-upgrades cron ntp fail2ban
|
||||
haveged pollinate \
|
||||
unattended-upgrades cron ntp fail2ban
|
||||
|
||||
# ### Set the system timezone
|
||||
#
|
||||
# Some systems are missing /etc/timezone, which we cat into the configs for
|
||||
# Z-Push and ownCloud, so we need to set it to something. Daily cron tasks
|
||||
# like the system backup are run at a time tied to the system timezone, so
|
||||
# letting the user choose will help us identify the right time to do those
|
||||
# things (i.e. late at night in whatever timezone the user actually lives
|
||||
# in).
|
||||
#
|
||||
# However, changing the timezone once it is set seems to confuse fail2ban
|
||||
# and requires restarting fail2ban (done below in the fail2ban
|
||||
# section) and syslog (see #328). There might be other issues, and it's
|
||||
# not likely the user will want to change this, so we only ask on first
|
||||
# setup.
|
||||
if [ -z "$NONINTERACTIVE" ]; then
|
||||
if [ ! -f /etc/timezone ] || [ ! -z $FIRST_TIME_SETUP ]; then
|
||||
# If the file is missing or this is the user's first time running
|
||||
# Mail-in-a-Box setup, run the interactive timezone configuration
|
||||
# tool.
|
||||
dpkg-reconfigure tzdata
|
||||
restart_service rsyslog
|
||||
fi
|
||||
else
|
||||
# This is a non-interactive setup so we can't ask the user.
|
||||
# If /etc/timezone is missing, set it to UTC.
|
||||
if [ ! -f /etc/timezone ]; then
|
||||
echo "Setting timezone to UTC."
|
||||
echo "Etc/UTC" > /etc/timezone
|
||||
restart_service rsyslog
|
||||
fi
|
||||
fi
|
||||
|
||||
# ### Seed /dev/urandom
|
||||
#
|
||||
# /dev/urandom is used by various components for generating random bytes for
|
||||
# encryption keys and passwords:
|
||||
#
|
||||
# * TLS private key (see `ssl.sh`, which calls `openssl genrsa`)
|
||||
# * DNSSEC signing keys (see `dns.sh`)
|
||||
# * our management server's API key (via Python's os.urandom method)
|
||||
# * Roundcube's SECRET_KEY (`webmail.sh`)
|
||||
# * ownCloud's administrator account password (`owncloud.sh`)
|
||||
#
|
||||
# Why /dev/urandom? It's the same as /dev/random, except that it doesn't wait
|
||||
# for a constant new stream of entropy. In practice, we only need a little
|
||||
# entropy at the start to get going. After that, we can safely pull a random
|
||||
# stream from /dev/urandom and not worry about how much entropy has been
|
||||
# added to the stream. (http://www.2uo.de/myths-about-urandom/) So we need
|
||||
# to worry about /dev/urandom being seeded properly (which is also an issue
|
||||
# for /dev/random), but after that /dev/urandom is superior to /dev/random
|
||||
# because it's faster and doesn't block indefinitely to wait for hardware
|
||||
# entropy. Note that `openssl genrsa` even uses `/dev/urandom`, and if it's
|
||||
# good enough for generating an RSA private key, it's good enough for anything
|
||||
# else we may need.
|
||||
#
|
||||
# Now about that seeding issue....
|
||||
#
|
||||
# /dev/urandom is seeded from "the uninitialized contents of the pool buffers when
|
||||
# the kernel starts, the startup clock time in nanosecond resolution,...and
|
||||
# entropy saved across boots to a local file" as well as the order of
|
||||
# execution of concurrent accesses to /dev/urandom. (Heninger et al 2012,
|
||||
# https://factorable.net/weakkeys12.conference.pdf) But when memory is zeroed,
|
||||
# the system clock is reset on boot, /etc/init.d/urandom has not yet run, or
|
||||
# the machine is single CPU or has no concurrent accesses to /dev/urandom prior
|
||||
# to this point, /dev/urandom may not be seeded well. After this, /dev/urandom
|
||||
# draws from the same entropy sources as /dev/random, but it doesn't block or
|
||||
# issue any warnings if no entropy is actually available. (http://www.2uo.de/myths-about-urandom/)
|
||||
# Entropy might not be readily available because this machine has no user input
|
||||
# devices (common on servers!) and either no hard disk or not enough IO has
|
||||
# ocurred yet --- although haveged tries to mitigate this. So there's a good chance
|
||||
# that accessing /dev/urandom will not be drawing from any hardware entropy and under
|
||||
# a perfect-storm circumstance where the other seeds are meaningless, /dev/urandom
|
||||
# may not be seeded at all.
|
||||
#
|
||||
# The first thing we'll do is block until we can seed /dev/urandom with enough
|
||||
# hardware entropy to get going, by drawing from /dev/random. haveged makes this
|
||||
# less likely to stall for very long.
|
||||
|
||||
echo Initializing system random number generator...
|
||||
dd if=/dev/random of=/dev/urandom bs=1 count=32 2> /dev/null
|
||||
|
||||
# This is supposedly sufficient. But because we're not sure if hardware entropy
|
||||
# is really any good on virtualized systems, we'll also seed from Ubuntu's
|
||||
# pollinate servers:
|
||||
|
||||
pollinate -q -r
|
||||
|
||||
# Between these two, we really ought to be all set.
|
||||
|
||||
# ### Package maintenance
|
||||
#
|
||||
# Allow apt to install system updates automatically every day.
|
||||
|
||||
cat > /etc/apt/apt.conf.d/02periodic <<EOF;
|
||||
@@ -138,7 +228,9 @@ restart_service resolvconf
|
||||
# ### Fail2Ban Service
|
||||
|
||||
# Configure the Fail2Ban installation to prevent dumb bruce-force attacks against dovecot, postfix and ssh
|
||||
cp conf/fail2ban/jail.local /etc/fail2ban/jail.local
|
||||
cat conf/fail2ban/jail.local \
|
||||
| sed "s/PUBLIC_IP/$PUBLIC_IP/g" \
|
||||
> /etc/fail2ban/jail.local
|
||||
cp conf/fail2ban/dovecotimap.conf /etc/fail2ban/filter.d/dovecotimap.conf
|
||||
|
||||
restart_service fail2ban
|
||||
|
||||
@@ -22,8 +22,9 @@ source /etc/mailinabox.conf # load global vars
|
||||
echo "Installing Roundcube (webmail)..."
|
||||
apt_install \
|
||||
dbconfig-common \
|
||||
php5 php5-sqlite php5-mcrypt php5-intl php5-json php5-common php-auth php-net-smtp php-net-socket php-net-sieve php-mail-mime php-mail-mimedecode php-crypt-gpg php5-gd php5-pspell \
|
||||
php5 php5-sqlite php5-mcrypt php5-intl php5-json php5-common php-auth php-net-smtp php-net-socket php-net-sieve php-mail-mime php-crypt-gpg php5-gd php5-pspell \
|
||||
tinymce libjs-jquery libjs-jquery-mousewheel libmagic1
|
||||
apt_get_quiet remove php-mail-mimedecode # no longer needed since Roundcube 1.1.3
|
||||
|
||||
# We used to install Roundcube from Ubuntu, without triggering the dependencies #NODOC
|
||||
# on Apache and MySQL, by downloading the debs and installing them manually. #NODOC
|
||||
@@ -33,8 +34,8 @@ apt-get purge -qq -y roundcube* #NODOC
|
||||
# Install Roundcube from source if it is not already present or if it is out of date.
|
||||
# Combine the Roundcube version number with the commit hash of vacation_sieve to track
|
||||
# whether we have the latest version.
|
||||
VERSION=1.1.2
|
||||
HASH=df88deae691da3ecf3e9f0aee674c1f3042ea1eb
|
||||
VERSION=1.1.3
|
||||
HASH=4513227bd64eb8564f056817341b1dfe478e215e
|
||||
VACATION_SIEVE_VERSION=91ea6f52216390073d1f5b70b5f6bea0bfaee7e5
|
||||
PERSISTENT_LOGIN_VERSION=117fbd8f93b56b2bf72ad055193464803ef3bc36
|
||||
HTML5_NOTIFIER_VERSION=046eb388dd63b1ec77a3ee485757fc25ae9e684d
|
||||
@@ -50,7 +51,7 @@ fi
|
||||
if [ $needs_update == 1 ]; then
|
||||
# install roundcube
|
||||
wget_verify \
|
||||
https://mailinabox.email/mirror/roundcubemail-$VERSION.tar.gz \
|
||||
https://downloads.sourceforge.net/project/roundcubemail/roundcubemail/$VERSION/roundcubemail-$VERSION.tar.gz \
|
||||
$HASH \
|
||||
/tmp/roundcube.tgz
|
||||
tar -C /usr/local/lib -zxf /tmp/roundcube.tgz
|
||||
@@ -74,7 +75,7 @@ fi
|
||||
# ### Configuring Roundcube
|
||||
|
||||
# Generate a safe 24-character secret key of safe characters.
|
||||
SECRET_KEY=$(dd if=/dev/random bs=1 count=18 2>/dev/null | base64 | fold -w 24 | head -n 1)
|
||||
SECRET_KEY=$(dd if=/dev/urandom bs=1 count=18 2>/dev/null | base64 | fold -w 24 | head -n 1)
|
||||
|
||||
# Create a configuration file.
|
||||
#
|
||||
|
||||
@@ -27,7 +27,7 @@ needs_update=0 #NODOC
|
||||
if [ ! -f /usr/local/lib/z-push/version ]; then
|
||||
needs_update=1 #NODOC
|
||||
elif [[ $TARGETHASH != `cat /usr/local/lib/z-push/version` ]]; then
|
||||
# checks if the version
|
||||
# checks if the version
|
||||
needs_update=1 #NODOC
|
||||
fi
|
||||
if [ $needs_update == 1 ]; then
|
||||
@@ -44,6 +44,7 @@ sed -i "s/define('BACKEND_PROVIDER', .*/define('BACKEND_PROVIDER', 'BackendCombi
|
||||
sed -i "s/define('USE_FULLEMAIL_FOR_LOGIN', .*/define('USE_FULLEMAIL_FOR_LOGIN', true);/" /usr/local/lib/z-push/config.php
|
||||
sed -i "s/define('LOG_MEMORY_PROFILER', .*/define('LOG_MEMORY_PROFILER', false);/" /usr/local/lib/z-push/config.php
|
||||
sed -i "s/define('BUG68532FIXED', .*/define('BUG68532FIXED', false);/" /usr/local/lib/z-push/config.php
|
||||
sed -i "s/define('LOGLEVEL', .*/define('LOGLEVEL', LOGLEVEL_ERROR);/" /usr/local/lib/z-push/config.php
|
||||
|
||||
# Configure BACKEND
|
||||
rm -f /usr/local/lib/z-push/backend/combined/config.php
|
||||
@@ -75,6 +76,19 @@ chmod 750 /var/lib/z-push
|
||||
chown www-data:www-data /var/log/z-push
|
||||
chown www-data:www-data /var/lib/z-push
|
||||
|
||||
# Add log rotation
|
||||
|
||||
cat > /etc/logrotate.d/z-push <<EOF;
|
||||
/var/log/z-push/*.log {
|
||||
weekly
|
||||
missingok
|
||||
rotate 52
|
||||
compress
|
||||
delaycompress
|
||||
notifempty
|
||||
}
|
||||
EOF
|
||||
|
||||
# Restart service.
|
||||
|
||||
restart_service php5-fpm
|
||||
|
||||
@@ -458,7 +458,6 @@ class BashScript(Grammar):
|
||||
|
||||
v = re.sub(r"(\$?)PRIMARY_HOSTNAME", r"<b>box.yourdomain.com</b>", v)
|
||||
v = re.sub(r"\$STORAGE_ROOT", r"<b>$STORE</b>", v)
|
||||
v = re.sub(r"\$CSR_COUNTRY", r"<b>US</b>", v)
|
||||
v = v.replace("`pwd`", "<code><b>/path/to/mailinabox</b></code>")
|
||||
|
||||
return v
|
||||
|
||||
Reference in New Issue
Block a user