mirror of
https://github.com/simple-login/app.git
synced 2024-09-28 20:51:29 +02:00
Merge pull request #153 from simple-login/dns
Add DMARC, improve DNS setup
This commit is contained in:
commit
9991723f5e
@ -9,7 +9,7 @@
|
||||
|
||||
{% block domain_detail_content %}
|
||||
<div class="bg-white p-4" style="max-width: 60rem; margin: auto">
|
||||
<h1 class="h3"> {{ custom_domain.domain }} </h1>
|
||||
<h1 class="h2"> {{ custom_domain.domain }} </h1>
|
||||
<div class="">Please follow the steps below to set up your domain.</div>
|
||||
|
||||
<div class="small-text mb-5">
|
||||
@ -28,19 +28,21 @@
|
||||
</div>
|
||||
|
||||
<div class="mb-2">Add the following MX DNS record to your domain. <br>
|
||||
Please note that there's a point (<em>.</em>) at the end target addresses. <br>
|
||||
Please note that there's a point (<em>.</em>) at the end target addresses.
|
||||
This is to make sure the <i>absolute</i> address is used.
|
||||
<br>
|
||||
Also some domain registrars (Namecheap, CloudFlare, etc) might use <em>@</em> for the root domain.
|
||||
</div>
|
||||
|
||||
{% for priority, email_server in EMAIL_SERVERS_WITH_PRIORITY %}
|
||||
<div class="mb-3 p-3" style="background-color: #eee">
|
||||
Domain: <em>{{ custom_domain.domain }}</em> or <em>@</em> <br>
|
||||
Record: MX <br>
|
||||
Domain: {{ custom_domain.domain }} or @ <br>
|
||||
Priority: {{ priority }} <br>
|
||||
Target: <em>{{ email_server }}</em>
|
||||
<button class="ml-4 clipboard btn btn-sm btn-outline-success" data-clipboard-action="copy"
|
||||
data-clipboard-text="{{ email_server }}">
|
||||
Copy <i class="fe fe-clipboard"></i>
|
||||
</button>
|
||||
Target: <em data-toggle="tooltip"
|
||||
title="Click to copy"
|
||||
class="clipboard"
|
||||
data-clipboard-text="{{ email_server }}">{{ email_server }}</em>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
@ -93,18 +95,18 @@
|
||||
Setting up SPF is highly recommended to reduce the chance your emails ending up in the recipient's Spam folder.
|
||||
</div>
|
||||
|
||||
<div class="mb-2">Add the following TXT DNS record to your domain</div>
|
||||
<div class="mb-2">Add the following TXT DNS record to your domain.</div>
|
||||
|
||||
<div class="mb-2 p-3" style="background-color: #eee">
|
||||
Domain: <em>{{ custom_domain.domain }}</em> or <em>@</em> <br>
|
||||
Record: TXT <br>
|
||||
Domain: {{ custom_domain.domain }} or @ <br>
|
||||
Value:
|
||||
<em>
|
||||
<em data-toggle="tooltip"
|
||||
title="Click to copy"
|
||||
class="clipboard"
|
||||
data-clipboard-text="{{ spf_record }}">
|
||||
{{ spf_record }}
|
||||
</em>
|
||||
<button class="ml-4 clipboard btn btn-sm btn-outline-success" data-clipboard-action="copy"
|
||||
data-clipboard-text="{{ spf_record }}">
|
||||
Copy <i class="fe fe-clipboard"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form method="post" action="#spf-form">
|
||||
@ -158,18 +160,21 @@
|
||||
Setting up DKIM is highly recommended to reduce the chance your emails ending up in the recipient's Spam folder.
|
||||
</div>
|
||||
|
||||
<div class="mb-2">Add the following TXT DNS record to your domain</div>
|
||||
<div class="mb-2">Add the following CNAME DNS record to your domain.</div>
|
||||
|
||||
<div class="mb-2 p-3" style="background-color: #eee">
|
||||
Domain: <em>dkim._domainkey.{{ custom_domain.domain }}</em> <br>
|
||||
Record: CNAME <br>
|
||||
Domain: <em data-toggle="tooltip"
|
||||
title="Click to copy"
|
||||
class="clipboard"
|
||||
data-clipboard-text="dkim._domainkey">dkim._domainkey</em>.{{ custom_domain.domain }} <br>
|
||||
Value:
|
||||
<em style="overflow-wrap: break-word">
|
||||
{{ dkim_record }}
|
||||
<em data-toggle="tooltip"
|
||||
title="Click to copy"
|
||||
class="clipboard"
|
||||
data-clipboard-text="{{ dkim_cname }}" style="overflow-wrap: break-word">
|
||||
{{ dkim_cname }}
|
||||
</em>
|
||||
<button class="ml-4 clipboard btn btn-sm btn-outline-success" data-clipboard-action="copy"
|
||||
data-clipboard-text="{{ dkim_record }}">
|
||||
Copy <i class="fe fe-clipboard"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form method="post" action="#dkim-form">
|
||||
@ -189,7 +194,7 @@
|
||||
<div class="text-danger mt-4">
|
||||
Your DNS is not correctly set.
|
||||
{% if dkim_errors %}
|
||||
The TXT record we obtain for
|
||||
The CNAME record we obtain for
|
||||
<em>dkim._domainkey.{{ custom_domain.domain }}</em> is:
|
||||
|
||||
<div class="mb-3 p-3" style="background-color: #eee">
|
||||
@ -206,5 +211,73 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div id="dmarc-form">
|
||||
<div class="font-weight-bold">4. DMARC (Optional)
|
||||
{% if custom_domain.dmarc_verified %}
|
||||
<span class="cursor" data-toggle="tooltip" data-original-title="DMARC Verified">✅</span>
|
||||
{% else %}
|
||||
<span class="cursor" data-toggle="tooltip" data-original-title="DMARC Not Verified">🚫 </span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
DMARC <a href="https://en.wikipedia.org/wiki/DMARC" target="_blank">(Wikipedia↗)</a>
|
||||
is designed to protect the domain from unauthorized use, commonly known as email spoofing. <br>
|
||||
Built around SPF and DKIM, a DMARC policy tells the receiving mail server what to do if
|
||||
neither of those authentication methods passes.
|
||||
</div>
|
||||
|
||||
<div class="mb-2">Add the following TXT DNS record to your domain.</div>
|
||||
|
||||
<div class="mb-2 p-3" style="background-color: #eee">
|
||||
Record: TXT <br>
|
||||
Domain: <em data-toggle="tooltip"
|
||||
title="Click to copy"
|
||||
class="clipboard"
|
||||
data-clipboard-text="_dmarc">_dmarc</em>.{{ custom_domain.domain }} <br>
|
||||
Value:
|
||||
<em data-toggle="tooltip"
|
||||
title="Click to copy"
|
||||
class="clipboard"
|
||||
data-clipboard-text="{{ dmarc_record }}">
|
||||
{{ dmarc_record }}
|
||||
</em>
|
||||
</div>
|
||||
|
||||
<form method="post" action="#dmarc-form">
|
||||
<input type="hidden" name="form-name" value="check-dmarc">
|
||||
{% if custom_domain.dmarc_verified %}
|
||||
<button type="submit" class="btn btn-outline-primary">
|
||||
Re-verify
|
||||
</button>
|
||||
{% else %}
|
||||
<button type="submit" class="btn btn-primary">
|
||||
Verify
|
||||
</button>
|
||||
{% endif %}
|
||||
</form>
|
||||
|
||||
{% if not dmarc_ok %}
|
||||
<div class="text-danger mt-4">
|
||||
Your DNS is not correctly set.
|
||||
The TXT record we obtain is:
|
||||
<div class="mb-3 p-3" style="background-color: #eee">
|
||||
{% if not dmarc_errors %}
|
||||
(Empty)
|
||||
{% endif %}
|
||||
|
||||
{% for r in dmarc_errors %}
|
||||
{{ r }} <br>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if custom_domain.dmarc_verified %}
|
||||
Without DMARC setup, emails sent from your alias might end up in the Spam/Junk folder.
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -1,13 +1,13 @@
|
||||
from flask import render_template, request, redirect, url_for, flash
|
||||
from flask_login import login_required, current_user
|
||||
|
||||
from app.config import EMAIL_SERVERS_WITH_PRIORITY, DKIM_DNS_VALUE, EMAIL_DOMAIN
|
||||
from app.config import EMAIL_SERVERS_WITH_PRIORITY, EMAIL_DOMAIN
|
||||
from app.dashboard.base import dashboard_bp
|
||||
from app.dns_utils import (
|
||||
get_mx_domains,
|
||||
get_spf_domain,
|
||||
get_dkim_record,
|
||||
get_txt_record,
|
||||
get_cname_record,
|
||||
)
|
||||
from app.extensions import db
|
||||
from app.models import CustomDomain, Alias
|
||||
@ -21,8 +21,15 @@ def domain_detail_dns(custom_domain_id):
|
||||
flash("You cannot see this page", "warning")
|
||||
return redirect(url_for("dashboard.index"))
|
||||
|
||||
mx_ok = spf_ok = dkim_ok = True
|
||||
mx_errors = spf_errors = dkim_errors = []
|
||||
spf_record = f"v=spf1 include:{EMAIL_DOMAIN} -all"
|
||||
|
||||
# hardcode the DKIM selector here
|
||||
dkim_cname = f"dkim._domainkey.{EMAIL_DOMAIN}"
|
||||
|
||||
dmarc_record = "v=DMARC1; p=quarantine; pct=100; adkim=s; aspf=s"
|
||||
|
||||
mx_ok = spf_ok = dkim_ok = dmarc_ok = True
|
||||
mx_errors = spf_errors = dkim_errors = dmarc_errors = []
|
||||
|
||||
if request.method == "POST":
|
||||
if request.form.get("form-name") == "check-mx":
|
||||
@ -37,7 +44,7 @@ def domain_detail_dns(custom_domain_id):
|
||||
]
|
||||
else:
|
||||
flash(
|
||||
"Your domain is verified. Now it can be used to create custom alias",
|
||||
"Your domain can start receiving emails. You can now use it to create alias",
|
||||
"success",
|
||||
)
|
||||
custom_domain.verified = True
|
||||
@ -52,7 +59,7 @@ def domain_detail_dns(custom_domain_id):
|
||||
if EMAIL_DOMAIN in spf_domains:
|
||||
custom_domain.spf_verified = True
|
||||
db.session.commit()
|
||||
flash("The SPF is setup correctly", "success")
|
||||
flash("SPF is setup correctly", "success")
|
||||
return redirect(
|
||||
url_for(
|
||||
"dashboard.domain_detail_dns", custom_domain_id=custom_domain.id
|
||||
@ -67,10 +74,9 @@ def domain_detail_dns(custom_domain_id):
|
||||
spf_errors = get_txt_record(custom_domain.domain)
|
||||
|
||||
elif request.form.get("form-name") == "check-dkim":
|
||||
dkim_record = get_dkim_record(custom_domain.domain)
|
||||
correct_dkim_record = f"v=DKIM1; k=rsa; p={DKIM_DNS_VALUE}"
|
||||
if dkim_record == correct_dkim_record:
|
||||
flash("The DKIM is setup correctly.", "success")
|
||||
dkim_record = get_cname_record(custom_domain.domain)
|
||||
if dkim_record == dkim_cname:
|
||||
flash("DKIM is setup correctly.", "success")
|
||||
custom_domain.dkim_verified = True
|
||||
db.session.commit()
|
||||
|
||||
@ -80,13 +86,27 @@ def domain_detail_dns(custom_domain_id):
|
||||
)
|
||||
)
|
||||
else:
|
||||
flash("DKIM: the TXT record is not correctly set", "warning")
|
||||
flash("DKIM: the CNAME record is not correctly set", "warning")
|
||||
dkim_ok = False
|
||||
dkim_errors = get_txt_record(f"dkim._domainkey.{custom_domain.domain}")
|
||||
dkim_errors = [dkim_record or "[Empty]"]
|
||||
|
||||
spf_record = f"v=spf1 include:{EMAIL_DOMAIN} -all"
|
||||
|
||||
dkim_record = f"v=DKIM1; k=rsa; p={DKIM_DNS_VALUE}"
|
||||
elif request.form.get("form-name") == "check-dmarc":
|
||||
txt_records = get_txt_record("_dmarc." + custom_domain.domain)
|
||||
if dmarc_record in txt_records:
|
||||
custom_domain.dmarc_verified = True
|
||||
db.session.commit()
|
||||
flash("DMARC is setup correctly", "success")
|
||||
return redirect(
|
||||
url_for(
|
||||
"dashboard.domain_detail_dns", custom_domain_id=custom_domain.id
|
||||
)
|
||||
)
|
||||
else:
|
||||
flash(
|
||||
f"DMARC: The TXT record is not correctly set", "warning",
|
||||
)
|
||||
dmarc_ok = False
|
||||
dmarc_errors = txt_records
|
||||
|
||||
return render_template(
|
||||
"dashboard/domain_detail/dns.html",
|
||||
|
@ -1,3 +1,5 @@
|
||||
from typing import Optional
|
||||
|
||||
import dns.resolver
|
||||
|
||||
|
||||
@ -10,6 +12,19 @@ def _get_dns_resolver():
|
||||
return my_resolver
|
||||
|
||||
|
||||
def get_cname_record(hostname) -> Optional[str]:
|
||||
"""Return the CNAME record if exists for a domain"""
|
||||
try:
|
||||
answers = _get_dns_resolver().query(hostname, "CNAME")
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
for a in answers:
|
||||
return a
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_mx_domains(hostname) -> [(int, str)]:
|
||||
"""return list of (priority, domain name).
|
||||
domain name ends with a "." at the end.
|
||||
@ -56,6 +71,7 @@ def get_spf_domain(hostname) -> [str]:
|
||||
|
||||
|
||||
def get_txt_record(hostname) -> [str]:
|
||||
"""return all domains listed in *include:*"""
|
||||
try:
|
||||
answers = _get_dns_resolver().query(hostname, "TXT")
|
||||
except Exception:
|
||||
@ -63,24 +79,10 @@ def get_txt_record(hostname) -> [str]:
|
||||
|
||||
ret = []
|
||||
|
||||
for a in answers: # type: dns.rdtypes.ANY.TXT.TXT
|
||||
ret.append(a)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def get_dkim_record(hostname) -> str:
|
||||
"""query the dkim._domainkey.{hostname} record and returns its value"""
|
||||
try:
|
||||
answers = _get_dns_resolver().query(f"dkim._domainkey.{hostname}", "TXT")
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
ret = []
|
||||
for a in answers: # type: dns.rdtypes.ANY.TXT.TXT
|
||||
for record in a.strings:
|
||||
record = record.decode() # record is bytes
|
||||
|
||||
ret.append(record)
|
||||
|
||||
return "".join(ret)
|
||||
return ret
|
||||
|
@ -1043,6 +1043,9 @@ class CustomDomain(db.Model, ModelMixin):
|
||||
spf_verified = db.Column(
|
||||
db.Boolean, nullable=False, default=False, server_default="0"
|
||||
)
|
||||
dmarc_verified = db.Column(
|
||||
db.Boolean, nullable=False, default=False, server_default="0"
|
||||
)
|
||||
|
||||
# an alias is created automatically the first time it receives an email
|
||||
catch_all = db.Column(db.Boolean, nullable=False, default=False, server_default="0")
|
||||
|
29
migrations/versions/2020_050312_de1b457472e0_.py
Normal file
29
migrations/versions/2020_050312_de1b457472e0_.py
Normal file
@ -0,0 +1,29 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: de1b457472e0
|
||||
Revises: f939d67374e4
|
||||
Create Date: 2020-05-03 12:02:11.958152
|
||||
|
||||
"""
|
||||
import sqlalchemy_utils
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'de1b457472e0'
|
||||
down_revision = 'f939d67374e4'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('custom_domain', sa.Column('dmarc_verified', sa.Boolean(), server_default='0', nullable=False))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('custom_domain', 'dmarc_verified')
|
||||
# ### end Alembic commands ###
|
@ -23,8 +23,3 @@ def test_get_txt_record():
|
||||
|
||||
r = get_txt_record(_DOMAIN)
|
||||
assert len(r) > 0
|
||||
|
||||
|
||||
def test_get_dkim_record():
|
||||
r = get_dkim_record(_DOMAIN)
|
||||
assert r.startswith("v=DKIM1; k=rsa;")
|
||||
|
Loading…
Reference in New Issue
Block a user