Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
M
mailinabox
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Administrator
mailinabox
Commits
b6933a73
Commit
b6933a73
authored
Oct 10, 2015
by
Joshua Tauberer
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
provision and install free SSL certificates from Let's Encrypt
parent
5033042b
Changes
7
Show whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
408 additions
and
79 deletions
+408
-79
nginx.conf
conf/nginx.conf
+16
-5
daily_tasks.sh
management/daily_tasks.sh
+3
-0
ssl_certificates.py
management/ssl_certificates.py
+359
-29
status_checks.py
management/status_checks.py
+13
-29
web_update.py
management/web_update.py
+9
-14
management.sh
setup/management.sh
+5
-2
start.sh
setup/start.sh
+3
-0
No files found.
conf/nginx.conf
View file @
b6933a73
## $HOSTNAME
# Redirect all HTTP to HTTPS.
# Redirect all HTTP to HTTPS *except* the ACME challenges (Let's Encrypt SSL certificate
# domain validation challenges) path, which must be served over HTTP per the ACME spec
# (due to some Apache vulnerability).
server
{
listen
80
;
listen
[::]:80
;
...
...
@@ -12,10 +14,19 @@ server {
# error pages and in the "Server" HTTP-Header.
server_tokens
off
;
location
/
{
# Redirect using the 'return' directive and the built-in
# variable '$request_uri' to avoid any capturing, matching
# or evaluation of regular expressions.
return
301
https://
$HOSTNAME$request_uri
;
}
location
/.well-known/acme-challenge/
{
# This path must be served over HTTP for ACME domain validation.
# We map this to a special path where our SSL cert provisioning
# tool knows to store challenge response files.
alias
$STORAGE_ROOT
/ssl/lets_encrypt/acme_challenges/
;
}
}
# The secure HTTPS server.
...
...
management/daily_tasks.sh
View file @
b6933a73
...
...
@@ -4,5 +4,8 @@
# Take a backup.
management/backup.py | management/email_administrator.py
"Backup Status"
# Provision any new certificates for new domains or domains with expiring certificates.
management/ssl_certificates.py
--headless
| management/email_administrator.py
"Error Provisioning TLS Certificate"
# Run status checks and email the administrator if anything changed.
management/status_checks.py
--show-changes
| management/email_administrator.py
"Status Checks Change Notice"
management/ssl_certificates.py
100644 → 100755
View file @
b6933a73
#!/usr/bin/python3
# Utilities for installing and selecting SSL certificates.
import
os
,
os
.
path
,
re
,
shutil
from
utils
import
shell
,
safe_domain_name
from
utils
import
shell
,
safe_domain_name
,
sort_domains
# SELECTING SSL CERTIFICATES FOR USE IN WEB
def
get_ssl_certificates
(
env
):
# Scan all of the installed SSL certificates and map every domain
...
...
@@ -17,6 +20,8 @@ def get_ssl_certificates(env):
# List all of the files in the SSL directory and one level deep.
def
get_file_list
():
if
not
os
.
path
.
exists
(
ssl_root
):
return
for
fn
in
os
.
listdir
(
ssl_root
):
fn
=
os
.
path
.
join
(
ssl_root
,
fn
)
if
os
.
path
.
isfile
(
fn
):
...
...
@@ -82,10 +87,27 @@ def get_ssl_certificates(env):
# prefer one that is not self-signed
cert
.
issuer
!=
cert
.
subject
,
###########################################################
# The above lines ensure that valid certificates are chosen
# over invalid certificates. The lines below choose between
# multiple valid certificates available for this domain.
###########################################################
# 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
,
###########################################################
# We always choose the certificate that is good for the
# longest period of time. This is important for how we
# provision certificates for Let's Encrypt. To ensure that
# we don't re-provision every night, we have to ensure that
# if we choose to provison a certificate that it will
# *actually* be used so the provisioning logic knows it
# doesn't still need to provision a certificate for the
# domain.
###########################################################
# in case a certificate is installed in multiple paths,
# prefer the... lexicographically last one?
cert
.
_filename
,
...
...
@@ -96,46 +118,348 @@ def get_ssl_certificates(env):
"private-key"
:
cert
.
_private_key
.
_filename
,
"certificate"
:
cert
.
_filename
,
"primary-domain"
:
cert
.
_primary_domain
,
"certificate_object"
:
cert
,
}
return
ret
def
get_domain_ssl_files
(
domain
,
ssl_certificates
,
env
,
allow_missing_cert
=
False
):
# Get the
default paths
.
def
get_domain_ssl_files
(
domain
,
ssl_certificates
,
env
,
allow_missing_cert
=
False
,
raw
=
False
):
# Get the
system certificate info
.
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'
))
system_certificate
=
{
"private-key"
:
ssl_private_key
,
"certificate"
:
ssl_certificate
,
"primary-domain"
:
env
[
'PRIMARY_HOSTNAME'
],
"certificate_object"
:
load_pem
(
load_cert_chain
(
ssl_certificate
)[
0
]),
}
if
domain
==
env
[
'PRIMARY_HOSTNAME'
]:
# The primary domain must use the server certificate because
# it is hard-coded in some service configuration files.
return
s
sl_private_key
,
ssl_certificate
,
Non
e
return
s
ystem_certificat
e
wildcard_domain
=
re
.
sub
(
"^[^
\
.]+"
,
"*"
,
domain
)
if
domain
in
ssl_certificates
:
cert_info
=
ssl_certificates
[
domain
]
cert_type
=
"multi-domain"
return
ssl_certificates
[
domain
]
elif
wildcard_domain
in
ssl_certificates
:
cert_info
=
ssl_certificates
[
wildcard_domain
]
cert_type
=
"wildcard"
return
ssl_certificates
[
wildcard_domain
]
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
# No valid certificate is available for this domain! Return default files.
return
system_certificate
else
:
# No
certificate is available - and warn appropriately
.
# No
valid certificate is available for this domain
.
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'
])
# PROVISIONING CERTIFICATES FROM LETSENCRYPT
def
get_certificates_to_provision
(
env
):
# Get a list of domain names that we should now provision certificates
# for. Provision if a domain name has no valid certificate or if any
# certificate is expiring in 14 days. If provisioning anything, also
# provision certificates expiring within 30 days. The period between
# 14 and 30 days allows us to consolidate domains into multi-domain
# certificates for domains expiring around the same time.
from
web_update
import
get_web_domains
import
datetime
now
=
datetime
.
datetime
.
utcnow
()
# Get domains with missing & expiring certificates.
certs
=
get_ssl_certificates
(
env
)
domains
=
set
()
domains_if_any
=
set
()
for
domain
in
get_web_domains
(
env
):
try
:
cert
=
get_domain_ssl_files
(
domain
,
certs
,
env
,
allow_missing_cert
=
True
)
except
FileNotFoundError
:
# system certificate is not present
continue
if
cert
is
None
:
# No valid certificate available.
domains
.
add
(
domain
)
else
:
via
=
None
# don't show a hint - show expiration info instead
cert
=
cert
[
"certificate_object"
]
if
cert
.
issuer
==
cert
.
subject
:
# This is self-signed. Get a real one.
domains
.
add
(
domain
)
# Valid certificate today, but is it expiring soon?
elif
cert
.
not_valid_after
-
now
<
datetime
.
timedelta
(
days
=
14
):
domains
.
add
(
domain
)
elif
cert
.
not_valid_after
-
now
<
datetime
.
timedelta
(
days
=
30
):
domains_if_any
.
add
(
domain
)
# Filter out domains that don't have correct DNS, because then the CA
# won't be able to do DNS validation.
def
is_domain_dns_correct
(
domain
):
# Must make qname absolute to prevent a fall-back lookup with a
# search domain appended.
import
dns.resolver
try
:
response
=
dns
.
resolver
.
query
(
domain
+
"."
,
"A"
)
except
:
return
False
return
len
(
response
)
==
1
and
str
(
response
[
0
])
==
env
[
"PUBLIC_IP"
]
domains
=
set
(
d
for
d
in
domains
if
is_domain_dns_correct
(
d
))
domains_if_any
=
set
(
d
for
d
in
domains_if_any
if
is_domain_dns_correct
(
d
))
# If there are any domains we definitely will provision for, add in
# additional domains to do at this time.
if
len
(
domains
)
>
0
:
domains
|=
domains_if_any
# Sort, just to keep related domain names together in the next step.
domains
=
sort_domains
(
domains
,
env
)
# Break into groups of up to 100 certificates at a time, which is Let's Encrypt's
# limit for a single certificate.
cert_domains
=
[]
while
len
(
domains
)
>
0
:
cert_domains
.
append
(
domains
[
0
:
100
]
)
domains
=
domains
[
100
:]
# Return a list of lists of domain names.
return
cert_domains
def
provision_certificates
(
env
,
agree_to_tos_url
=
None
,
logger
=
None
):
import
requests.exceptions
import
acme.messages
from
free_tls_certificates
import
client
# What domains to provision certificates for? This is a list of
# lists of domains.
certs
=
get_certificates_to_provision
(
env
)
if
len
(
certs
)
==
0
:
return
{
"requests"
:
[],
}
# Prepare to provision.
# Where should we put our Let's Encrypt account info and state cache.
account_path
=
os
.
path
.
join
(
env
[
'STORAGE_ROOT'
],
'ssl/lets_encrypt'
)
if
not
os
.
path
.
exists
(
account_path
):
os
.
mkdir
(
account_path
)
# Where should we put ACME challenge files. This is mapped to /.well-known/acme_challenge
# by the nginx configuration.
challenges_path
=
os
.
path
.
join
(
account_path
,
'acme_challenges'
)
if
not
os
.
path
.
exists
(
challenges_path
):
os
.
mkdir
(
challenges_path
)
# Read in the private key that we use for all TLS certificates. We'll need that
# to generate a CSR (done by free_tls_certificates).
with
open
(
os
.
path
.
join
(
env
[
'STORAGE_ROOT'
],
'ssl/ssl_private_key.pem'
),
'rb'
)
as
f
:
private_key
=
f
.
read
()
return
cert_info
[
'private-key'
],
cert_info
[
'certificate'
],
via
# Provision certificates.
ret
=
[]
for
domain_list
in
certs
:
# For return.
ret_item
=
{
"domains"
:
domain_list
,
"log"
:
[],
}
ret
.
append
(
ret_item
)
# Logging for free_tls_certificates.
def
my_logger
(
message
):
if
logger
:
logger
(
message
)
ret_item
[
"log"
]
.
append
(
message
)
# Attempt to provision a certificate.
try
:
try
:
cert
=
client
.
issue_certificate
(
domain_list
,
account_path
,
agree_to_tos_url
=
agree_to_tos_url
,
private_key
=
private_key
,
logger
=
my_logger
)
except
client
.
NeedToTakeAction
as
e
:
# Write out the ACME challenge files.
for
action
in
e
.
actions
:
if
isinstance
(
action
,
client
.
NeedToInstallFile
):
fn
=
os
.
path
.
join
(
challenges_path
,
action
.
file_name
)
with
open
(
fn
,
'w'
)
as
f
:
f
.
write
(
action
.
contents
)
else
:
raise
ValueError
(
str
(
action
))
# Try to provision now that the challenge files are installed.
cert
=
client
.
issue_certificate
(
domain_list
,
account_path
,
private_key
=
private_key
,
logger
=
my_logger
)
except
client
.
NeedToAgreeToTOS
as
e
:
# The user must agree to the Let's Encrypt terms of service agreement
# before any further action can be taken.
ret_item
.
update
({
"result"
:
"agree-to-tos"
,
"url"
:
e
.
url
,
})
except
client
.
WaitABit
as
e
:
# We need to hold on for a bit before querying again to see if we can
# acquire a provisioned certificate.
import
time
,
datetime
ret_item
.
update
({
"result"
:
"wait"
,
"until"
:
e
.
until_when
,
#.isoformat(),
"seconds"
:
(
e
.
until_when
-
datetime
.
datetime
.
now
())
.
total_seconds
()
})
except
client
.
AccountDataIsCorrupt
as
e
:
# This is an extremely rare condition.
ret_item
.
update
({
"result"
:
"error"
,
"message"
:
"Something unexpected went wrong. It looks like your local Let's Encrypt account data is corrupted. There was a problem with the file "
+
e
.
account_file_path
+
"."
,
})
except
(
client
.
InvalidDomainName
,
client
.
NeedToTakeAction
,
acme
.
messages
.
Error
,
requests
.
exceptions
.
RequestException
)
as
e
:
ret_item
.
update
({
"result"
:
"error"
,
"message"
:
"Something unexpected went wrong: "
+
str
(
e
),
})
else
:
# A certificate was issued.
install_status
=
install_cert
(
domain_list
[
0
],
cert
[
'cert'
]
.
decode
(
"ascii"
),
b
"
\n
"
.
join
(
cert
[
'chain'
])
.
decode
(
"ascii"
),
env
,
raw
=
True
)
# str indicates the certificate was not installed.
if
isinstance
(
install_status
,
str
):
ret_item
.
update
({
"result"
:
"error"
,
"message"
:
"Something unexpected was wrong with the provisioned certificate: "
+
install_status
,
})
else
:
# A list indicates success and what happened next.
ret_item
[
"log"
]
.
extend
(
install_status
)
ret_item
.
update
({
"result"
:
"installed"
,
})
# Return what happened with each certificate request.
return
{
"requests"
:
ret
}
def
provision_certificates_cmdline
():
import
sys
from
utils
import
load_environment
,
exclusive_process
exclusive_process
(
"update_tls_certificates"
)
env
=
load_environment
()
agree_to_tos_url
=
None
while
True
:
# Run the provisioning script. This installs certificates. If there are
# a very large number of domains on this box, it issues separate
# certificates for groups of domains. We have to check the result for
# each group.
def
my_logger
(
message
):
if
"-v"
in
sys
.
argv
:
print
(
">"
,
message
)
status
=
provision_certificates
(
env
,
agree_to_tos_url
=
agree_to_tos_url
,
logger
=
my_logger
)
agree_to_tos_url
=
None
# reset to prevent infinite looping
if
not
status
[
"requests"
]:
# No domains need certificates.
if
"--headless"
not
in
sys
.
argv
or
"-v"
in
sys
.
argv
:
print
(
"No domains hosted on this box need a certificate at this time."
)
sys
.
exit
(
0
)
# What happened?
wait_until
=
None
wait_domains
=
[]
for
request
in
status
[
"requests"
]:
if
request
[
"result"
]
==
"agree-to-tos"
:
# We may have asked already in a previous iteration.
if
agree_to_tos_url
is
not
None
:
continue
# Can't ask the user a question in this mode.
if
"--headless"
in
sys
.
argv
:
print
(
"Can't issue TLS certficate until user has agreed to Let's Encrypt TOS."
)
sys
.
exit
(
1
)
print
(
"""
I'm going to provision a TLS certificate (formerly called a SSL certificate)
for you from Let's Encrypt (letsencrypt.org).
TLS certificates are cryptographic keys that ensure communication between
you and this box are secure when getting and sending mail and visiting
websites hosted on this box. Let's Encrypt is a free provider of TLS
certificates.
Please open this document in your web browser:
%
s
It is Let's Encrypt's terms of service agreement. If you agree, I can
provision that TLS certificate. If you don't agree, you will have an
opportunity to install your own TLS certificate from the Mail-in-a-Box
control panel.
Do you agree to the agreement? Type Y or N and press <ENTER>: """
%
request
[
"url"
],
end
=
''
,
flush
=
True
)
if
sys
.
stdin
.
readline
()
.
strip
()
.
upper
()
!=
"Y"
:
print
(
"
\n
You didn't agree. Quitting."
)
sys
.
exit
(
1
)
# Okay, indicate agreement on next iteration.
agree_to_tos_url
=
request
[
"url"
]
if
request
[
"result"
]
==
"wait"
:
# Must wait. We'll record until when. The wait occurs below.
if
wait_until
is
None
:
wait_until
=
request
[
"until"
]
else
:
wait_until
=
max
(
wait_until
,
request
[
"until"
])
wait_domains
+=
request
[
"domains"
]
if
request
[
"result"
]
==
"error"
:
print
(
", "
.
join
(
request
[
"domains"
])
+
":"
)
print
(
request
[
"message"
])
if
request
[
"result"
]
==
"installed"
:
print
(
"A TLS certificate was successfully installed for "
+
", "
.
join
(
request
[
"domains"
])
+
"."
)
if
wait_until
:
# Wait, then loop.
import
time
,
datetime
print
()
print
(
"A TLS certificate was requested for: "
+
", "
.
join
(
wait_domains
)
+
"."
)
first
=
True
while
wait_until
>
datetime
.
datetime
.
now
():
if
"--headless"
not
in
sys
.
argv
or
first
:
print
(
"We have to wait"
,
int
(
round
((
wait_until
-
datetime
.
datetime
.
now
())
.
total_seconds
())),
"seconds for the certificate to be issued..."
)
time
.
sleep
(
10
)
first
=
False
continue
# Loop!
if
agree_to_tos_url
:
# The user agrees to the TOS. Loop to try again by agreeing.
continue
# Loop!
# Unless we were instructed to wait, or we just agreed to the TOS,
# we're done for now.
break
# INSTALLING A NEW CERTIFICATE FROM THE CONTROL PANEL
def
create_csr
(
domain
,
ssl_key
,
country_code
,
env
):
return
shell
(
"check_output"
,
[
...
...
@@ -144,7 +468,7 @@ def create_csr(domain, ssl_key, country_code, env):
"-sha256"
,
"-subj"
,
"/C=
%
s/ST=/L=/O=/CN=
%
s"
%
(
country_code
,
domain
)])
def
install_cert
(
domain
,
ssl_cert
,
ssl_chain
,
env
):
def
install_cert
(
domain
,
ssl_cert
,
ssl_chain
,
env
,
raw
=
False
):
# Write the combined cert+chain to a temporary path and validate that it is OK.
# The certificate always goes above the chain.
import
tempfile
...
...
@@ -203,8 +527,10 @@ def install_cert(domain, ssl_cert, ssl_chain, env):
# 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
)
)
if
raw
:
return
ret
return
"
\n
"
.
join
(
ret
)
# VALIDATION OF CERTIFICATES
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
...
...
@@ -305,16 +631,16 @@ def check_certificate(domain, ssl_certificate, ssl_private_key, warn_if_expiring
# 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
:
if
not
rounded_time
or
ndays
<=
10
:
# Yikes better renew soon!
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
:
# We'll renew it with Lets Encrypt.
expiry_info
=
"The certificate expires on
%
s."
%
cert_expiration_date
.
strftime
(
"
%
x"
)
if
ndays
<=
31
and
warn_if_expiring_soon
:
if
ndays
<=
10
and
warn_if_expiring_soon
:
# Warn on day 10 to give 4 days for us to automatically renew the
# certificate, which occurs on day 14.
return
(
"The certificate is expiring soon: "
+
expiry_info
,
None
)
# Return the special OK code.
...
...
@@ -381,3 +707,7 @@ def get_certificate_domains(cert):
pass
return
names
,
cn
if
__name__
==
"__main__"
:
# Provision certificates.
provision_certificates_cmdline
()
management/status_checks.py
View file @
b6933a73
...
...
@@ -278,23 +278,24 @@ def run_domain_checks(rounded_time, env, output, pool):
# Get the list of domains that we don't serve web for because of a custom CNAME/A record.
domains_with_a_records
=
get_domains_with_a_records
(
env
)
ssl_certificates
=
get_ssl_certificates
(
env
)
# Serial version:
#for domain in sort_domains(domains_to_check, env):
# run_domain_checks_on_domain(domain, rounded_time, env, dns_domains, dns_zonefiles, mail_domains, web_domains)
# Parallelize the checks across a worker pool.
args
=
((
domain
,
rounded_time
,
env
,
dns_domains
,
dns_zonefiles
,
mail_domains
,
web_domains
,
domains_with_a_records
,
ssl_certificates
)
args
=
((
domain
,
rounded_time
,
env
,
dns_domains
,
dns_zonefiles
,
mail_domains
,
web_domains
,
domains_with_a_records
)
for
domain
in
domains_to_check
)
ret
=
pool
.
starmap
(
run_domain_checks_on_domain
,
args
,
chunksize
=
1
)
ret
=
dict
(
ret
)
# (domain, output) => { domain: output }
for
domain
in
sort_domains
(
ret
,
env
):
ret
[
domain
]
.
playback
(
output
)
def
run_domain_checks_on_domain
(
domain
,
rounded_time
,
env
,
dns_domains
,
dns_zonefiles
,
mail_domains
,
web_domains
,
domains_with_a_records
,
ssl_certificates
):
def
run_domain_checks_on_domain
(
domain
,
rounded_time
,
env
,
dns_domains
,
dns_zonefiles
,
mail_domains
,
web_domains
,
domains_with_a_records
):
output
=
BufferedOutput
()
# we'd move this up, but this returns non-pickleable values
ssl_certificates
=
get_ssl_certificates
(
env
)
# The domain is IDNA-encoded in the database, but for display use Unicode.
try
:
domain_display
=
idna
.
decode
(
domain
.
encode
(
'ascii'
))
...
...
@@ -656,45 +657,28 @@ def check_ssl_cert(domain, rounded_time, ssl_certificates, env, output):
if
query_dns
(
domain
,
"A"
,
None
)
not
in
(
env
[
'PUBLIC_IP'
],
None
):
return
# Where is the SSL stored?
x
=
get_domain_ssl_files
(
domain
,
ssl_certificates
,
env
,
allow_missing_cert
=
True
)
if
x
is
None
:
tls_cert
=
get_domain_ssl_files
(
domain
,
ssl_certificates
,
env
,
allow_missing_cert
=
True
)
if
tls_cert
is
None
:
output
.
print_warning
(
"""No SSL certificate is installed for this domain. Visitors to a website on
this domain will get a security warning. If you are not serving a website on this domain, you do
not need to take any action. Use the SSL Certificates page in the control panel to install a
SSL certificate."""
)
return
ssl_key
,
ssl_certificate
,
ssl_via
=
x
# Check that the certificate is good.
cert_status
,
cert_status_details
=
check_certificate
(
domain
,
ssl_certificate
,
ssl_key
,
rounded_time
=
rounded_time
)
cert_status
,
cert_status_details
=
check_certificate
(
domain
,
tls_cert
[
"certificate"
],
tls_cert
[
"private-key"
]
,
rounded_time
=
rounded_time
)
if
cert_status
==
"OK"
:
# The certificate is ok. The details has expiry info.
output
.
print_ok
(
"SSL certificate is signed & valid.
%
s
%
s"
%
(
ssl_via
if
ssl_via
else
""
,
cert_status_details
)
)
output
.
print_ok
(
"SSL certificate is signed & valid.
"
+
cert_status_details
)
elif
cert_status
==
"SELF-SIGNED"
:
# Offer instructions for purchasing a signed certificate.
fingerprint
=
shell
(
'check_output'
,
[
"openssl"
,
"x509"
,
"-in"
,
ssl_certificate
,
"-noout"
,
"-fingerprint"
])
fingerprint
=
re
.
sub
(
".*Fingerprint="
,
""
,
fingerprint
)
.
strip
()
if
domain
==
env
[
'PRIMARY_HOSTNAME'
]:
output
.
print_error
(
"""The SSL certificate for this domain is currently self-signed. You will get a security
warning when you check or send email and when visiting this domain in a web browser (for webmail or
static site hosting). Use the SSL Certificates page in the control panel to install a signed SSL certificate.
You may choose to leave the self-signed certificate in place and confirm the security exception, but check that
the certificate fingerprint matches the following:"""
)
output
.
print_line
(
""
)
output
.
print_line
(
" "
+
fingerprint
,
monospace
=
True
)
static site hosting)."""
)
else
:
output
.
print_error
(
"""The SSL certificate for this domain is self-signed."""
)
...
...
@@ -927,10 +911,10 @@ if __name__ == "__main__":
if
query_dns
(
domain
,
"A"
)
!=
env
[
'PUBLIC_IP'
]:
sys
.
exit
(
1
)
ssl_certificates
=
get_ssl_certificates
(
env
)
ssl_key
,
ssl_certificate
,
ssl_via
=
get_domain_ssl_files
(
domain
,
ssl_certificates
,
env
)
if
not
os
.
path
.
exists
(
ssl_certificate
):
tls_cert
=
get_domain_ssl_files
(
domain
,
ssl_certificates
,
env
)
if
not
os
.
path
.
exists
(
tls_cert
[
"certificate"
]
):
sys
.
exit
(
1
)
cert_status
,
cert_status_details
=
check_certificate
(
domain
,
ssl_certificate
,
ssl_key
,
warn_if_expiring_soon
=
False
)
cert_status
,
cert_status_details
=
check_certificate
(
domain
,
tls_cert
[
"certificate"
],
tls_cert
[
"private-key"
]
,
warn_if_expiring_soon
=
False
)
if
cert_status
!=
"OK"
:
sys
.
exit
(
1
)
sys
.
exit
(
0
)
...
...
management/web_update.py
View file @
b6933a73
...
...
@@ -119,7 +119,7 @@ def make_domain_config(domain, templates, ssl_certificates, env):
root
=
get_web_root
(
domain
,
env
)
# What private key and SSL certificate will we use for this domain?
ssl_key
,
ssl_certificate
,
ssl_via
=
get_domain_ssl_files
(
domain
,
ssl_certificates
,
env
)
tls_cert
=
get_domain_ssl_files
(
domain
,
ssl_certificates
,
env
)
# ADDITIONAL DIRECTIVES.
...
...
@@ -136,7 +136,7 @@ def make_domain_config(domain, templates, ssl_certificates, env):
finally
:
f
.
close
()
return
sha1
.
hexdigest
()
nginx_conf_extra
+=
"# ssl files sha1:
%
s /
%
s
\n
"
%
(
hashfile
(
ssl_key
),
hashfile
(
ssl_certificate
))
nginx_conf_extra
+=
"# ssl files sha1:
%
s /
%
s
\n
"
%
(
hashfile
(
tls_cert
[
"private-key"
]),
hashfile
(
tls_cert
[
"certificate"
]
))
# Add in any user customizations in YAML format.
hsts
=
"yes"
...
...
@@ -177,8 +177,8 @@ def make_domain_config(domain, templates, ssl_certificates, env):
nginx_conf
=
nginx_conf
.
replace
(
"$STORAGE_ROOT"
,
env
[
'STORAGE_ROOT'
])
nginx_conf
=
nginx_conf
.
replace
(
"$HOSTNAME"
,
domain
)
nginx_conf
=
nginx_conf
.
replace
(
"$ROOT"
,
root
)
nginx_conf
=
nginx_conf
.
replace
(
"$SSL_KEY"
,
ssl_key
)
nginx_conf
=
nginx_conf
.
replace
(
"$SSL_CERTIFICATE"
,
ssl_certificate
)
nginx_conf
=
nginx_conf
.
replace
(
"$SSL_KEY"
,
tls_cert
[
"private-key"
]
)
nginx_conf
=
nginx_conf
.
replace
(
"$SSL_CERTIFICATE"
,
tls_cert
[
"certificate"
]
)
nginx_conf
=
nginx_conf
.
replace
(
"$REDIRECT_DOMAIN"
,
re
.
sub
(
r"^www\."
,
""
,
domain
))
# for default www redirects to parent domain
return
nginx_conf
...
...
@@ -193,20 +193,15 @@ def get_web_root(domain, env, test_exists=True):
def
get_web_domains_info
(
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
))
ssl_certificates
=
get_ssl_certificates
(
env
)
# for the SSL config panel, get cert status
def
check_cert
(
domain
):
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"
)
ssl_key
,
ssl_certificate
,
ssl_via
=
x
cert_status
,
cert_status_details
=
check_certificate
(
domain
,
ssl_certificate
,
ssl_key
)
tls_cert
=
get_domain_ssl_files
(
domain
,
ssl_certificates
,
env
,
allow_missing_cert
=
True
)
if
tls_cert
is
None
:
return
(
"danger"
,
"No Certificate Installed"
)
cert_status
,
cert_status_details
=
check_certificate
(
domain
,
tls_cert
[
"certificate"
],
tls_cert
[
"private-key"
])
if
cert_status
==
"OK"
:
if
not
ssl_via
:
return
(
"success"
,
"Signed & valid. "
+
cert_status_details
)
else
:
# This is an alternate domain but using the same cert as the primary domain.
return
(
"success"
,
"Signed & valid. "
+
ssl_via
)
elif
cert_status
==
"SELF-SIGNED"
:
return
(
"warning"
,
"Self-signed. Get a signed certificate to stop warnings."
)
else
:
...
...
setup/management.sh
View file @
b6933a73
...
...
@@ -11,8 +11,11 @@ if [ -f /usr/local/lib/python2.7/dist-packages/boto/__init__.py ]; then hide_out
# 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 psutil
# Install other Python packages. The first line is the packages that Josh maintains himself!
hide_output pip3
install
--upgrade
\
rtyaml
"email_validator>=1.0.0"
free_tls_certificates
\
"idna>=2.0.0"
"cryptography>=1.0.2"
boto psutil
# email_validator is repeated in setup/questions.sh
# Create a backup directory and a random key for encrypting backups.
...
...
@@ -44,5 +47,5 @@ cat > /etc/cron.d/mailinabox-nightly << EOF;
0 3 * * * root (cd `pwd` && management/daily_tasks.sh)
EOF
# Start
it
.
# Start
the management server
.
restart_service mailinabox
setup/start.sh
View file @
b6933a73
...
...
@@ -116,6 +116,9 @@ done
tools/dns_update
tools/web_update
# If DNS is already working, try to provision TLS certficates from Let's Encrypt.
management/ssl_certificates.py
# If there aren't any mail users yet, create one.
source
setup/firstuser.sh
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment