mirror of
https://github.com/laurivosandi/certidude
synced 2024-12-22 16:25:17 +00:00
Several improvements
* Add EC support * Make token form toggleable * Make client certificates compatible with iOS native IKEv2 * Fix OU for self-enroll * Improved sample scripts in web UI
This commit is contained in:
parent
9c6872a949
commit
577962e09b
@ -10,7 +10,7 @@ from xattr import listxattr, getxattr
|
|||||||
from certidude.auth import login_required
|
from certidude.auth import login_required
|
||||||
from certidude.user import User
|
from certidude.user import User
|
||||||
from certidude.decorators import serialize, csrf_protection
|
from certidude.decorators import serialize, csrf_protection
|
||||||
from certidude import const, config
|
from certidude import const, config, authority
|
||||||
from .utils import AuthorityHandler
|
from .utils import AuthorityHandler
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -140,8 +140,11 @@ class SessionResource(AuthorityHandler):
|
|||||||
offline = 600, # Seconds from last seen activity to consider lease offline, OpenVPN reneg-sec option
|
offline = 600, # Seconds from last seen activity to consider lease offline, OpenVPN reneg-sec option
|
||||||
dead = 604800 # Seconds from last activity to consider lease dead, X509 chain broken or machine discarded
|
dead = 604800 # Seconds from last activity to consider lease dead, X509 chain broken or machine discarded
|
||||||
),
|
),
|
||||||
common_name = const.FQDN,
|
certificate = dict(
|
||||||
title = self.authority.certificate.subject.native["common_name"],
|
algorithm = authority.public_key.algorithm,
|
||||||
|
common_name = self.authority.certificate.subject.native["common_name"],
|
||||||
|
blob = self.authority.certificate_buf.decode("ascii"),
|
||||||
|
),
|
||||||
mailer = dict(
|
mailer = dict(
|
||||||
name = config.MAILER_NAME,
|
name = config.MAILER_NAME,
|
||||||
address = config.MAILER_ADDRESS
|
address = config.MAILER_ADDRESS
|
||||||
@ -164,6 +167,7 @@ class SessionResource(AuthorityHandler):
|
|||||||
)
|
)
|
||||||
) if req.context.get("user").is_admin() else None,
|
) if req.context.get("user").is_admin() else None,
|
||||||
features=dict(
|
features=dict(
|
||||||
|
token=bool(config.TOKEN_URL),
|
||||||
tagging=True,
|
tagging=True,
|
||||||
leases=True,
|
leases=True,
|
||||||
logging=config.LOGGING_BACKEND))
|
logging=config.LOGGING_BACKEND))
|
||||||
|
@ -41,7 +41,8 @@ class TokenResource(AuthorityHandler):
|
|||||||
common_name = csr["certification_request_info"]["subject"].native["common_name"]
|
common_name = csr["certification_request_info"]["subject"].native["common_name"]
|
||||||
assert common_name == username or common_name.startswith(username + "@"), "Invalid common name %s" % common_name
|
assert common_name == username or common_name.startswith(username + "@"), "Invalid common name %s" % common_name
|
||||||
try:
|
try:
|
||||||
_, resp.body = self.authority._sign(csr, body, profile="default")
|
_, resp.body = self.authority._sign(csr, body, profile="default",
|
||||||
|
overwrite=config.TOKEN_OVERWRITE_PERMITTED)
|
||||||
resp.set_header("Content-Type", "application/x-pem-file")
|
resp.set_header("Content-Type", "application/x-pem-file")
|
||||||
logger.info("Autosigned %s as proven by token ownership", common_name)
|
logger.info("Autosigned %s as proven by token ownership", common_name)
|
||||||
except FileExistsError:
|
except FileExistsError:
|
||||||
|
@ -47,11 +47,16 @@ def self_enroll():
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
path, buf, cert, signed, expires = get_signed(common_name)
|
path, buf, cert, signed, expires = get_signed(common_name)
|
||||||
public_key = asymmetric.load_public_key(path)
|
self_public_key = asymmetric.load_public_key(path)
|
||||||
private_key = asymmetric.load_private_key(self_key_path)
|
private_key = asymmetric.load_private_key(self_key_path)
|
||||||
except FileNotFoundError: # certificate or private key not found
|
except FileNotFoundError: # certificate or private key not found
|
||||||
with open(self_key_path, 'wb') as fh:
|
with open(self_key_path, 'wb') as fh:
|
||||||
public_key, private_key = asymmetric.generate_pair('rsa', bit_size=2048)
|
if public_key.algorithm == "ec":
|
||||||
|
self_public_key, private_key = asymmetric.generate_pair("ec", curve=public_key.curve)
|
||||||
|
elif public_key.algorithm == "rsa":
|
||||||
|
self_public_key, private_key = asymmetric.generate_pair("rsa", bit_size=public_key.bit_size)
|
||||||
|
else:
|
||||||
|
NotImplemented
|
||||||
fh.write(asymmetric.dump_private_key(private_key, None))
|
fh.write(asymmetric.dump_private_key(private_key, None))
|
||||||
else:
|
else:
|
||||||
now = datetime.utcnow()
|
now = datetime.utcnow()
|
||||||
@ -59,7 +64,7 @@ def self_enroll():
|
|||||||
click.echo("Certificate %s still valid, delete to self-enroll again" % path)
|
click.echo("Certificate %s still valid, delete to self-enroll again" % path)
|
||||||
return
|
return
|
||||||
|
|
||||||
builder = CSRBuilder({"common_name": common_name}, public_key)
|
builder = CSRBuilder({"common_name": common_name}, self_public_key)
|
||||||
request = builder.build(private_key)
|
request = builder.build(private_key)
|
||||||
with open(os.path.join(directory, "requests", common_name + ".pem"), "wb") as fh:
|
with open(os.path.join(directory, "requests", common_name + ".pem"), "wb") as fh:
|
||||||
fh.write(pem_armor_csr(request))
|
fh.write(pem_armor_csr(request))
|
||||||
@ -389,6 +394,7 @@ def _sign(csr, buf, skip_notify=False, skip_push=False, overwrite=False, profile
|
|||||||
builder.subject_alt_domains = [common_name] # OpenVPN uses CN while StrongSwan uses SAN to match hostname of the server
|
builder.subject_alt_domains = [common_name] # OpenVPN uses CN while StrongSwan uses SAN to match hostname of the server
|
||||||
builder.extended_key_usage = set(["server_auth", "1.3.6.1.5.5.8.2.2", "client_auth"])
|
builder.extended_key_usage = set(["server_auth", "1.3.6.1.5.5.8.2.2", "client_auth"])
|
||||||
else:
|
else:
|
||||||
|
builder.subject_alt_domains = [common_name] # iOS demands SAN also for clients
|
||||||
builder.extended_key_usage = set(["client_auth"])
|
builder.extended_key_usage = set(["client_auth"])
|
||||||
|
|
||||||
end_entity_cert = builder.build(private_key)
|
end_entity_cert = builder.build(private_key)
|
||||||
|
@ -287,7 +287,17 @@ def certidude_enroll(fork, renew, no_wait, kerberos, skip_self):
|
|||||||
if not os.path.exists(request_path):
|
if not os.path.exists(request_path):
|
||||||
key_partial = key_path + ".part"
|
key_partial = key_path + ".part"
|
||||||
request_partial = request_path + ".part"
|
request_partial = request_path + ".part"
|
||||||
public_key, private_key = asymmetric.generate_pair('rsa', bit_size=2048)
|
|
||||||
|
certificate = x509.Certificate.load(certificate_der_bytes)
|
||||||
|
public_key = asymmetric.load_public_key(certificate["tbs_certificate"]["subject_public_key_info"])
|
||||||
|
|
||||||
|
if public_key.algorithm == "ec":
|
||||||
|
self_public_key, private_key = asymmetric.generate_pair("ec", curve=public_key.curve)
|
||||||
|
elif public_key.algorithm == "rsa":
|
||||||
|
self_public_key, private_key = asymmetric.generate_pair("rsa", bit_size=public_key.bit_size)
|
||||||
|
else:
|
||||||
|
NotImplemented
|
||||||
|
|
||||||
builder = CSRBuilder({"common_name": common_name}, public_key)
|
builder = CSRBuilder({"common_name": common_name}, public_key)
|
||||||
request = builder.build(private_key)
|
request = builder.build(private_key)
|
||||||
with open(key_partial, 'wb') as f:
|
with open(key_partial, 'wb') as f:
|
||||||
@ -945,8 +955,9 @@ def certidude_setup_openvpn_networkmanager(authority, remote, common_name, **pat
|
|||||||
@click.option("--server-flags", is_flag=True, help="Add TLS Server and IKE Intermediate extended key usage flags")
|
@click.option("--server-flags", is_flag=True, help="Add TLS Server and IKE Intermediate extended key usage flags")
|
||||||
@click.option("--outbox", default="smtp://smtp.%s" % const.DOMAIN, help="SMTP server, smtp://smtp.%s by default" % const.DOMAIN)
|
@click.option("--outbox", default="smtp://smtp.%s" % const.DOMAIN, help="SMTP server, smtp://smtp.%s by default" % const.DOMAIN)
|
||||||
@click.option("--skip-packages", is_flag=True, help="Don't attempt to install apt/pip/npm packages")
|
@click.option("--skip-packages", is_flag=True, help="Don't attempt to install apt/pip/npm packages")
|
||||||
|
@click.option("--elliptic-curve", "-e", is_flag=True, help="Generate EC instead of RSA keypair")
|
||||||
@fqdn_required
|
@fqdn_required
|
||||||
def certidude_setup_authority(username, kerberos_keytab, nginx_config, country, state, locality, organization, organizational_unit, common_name, directory, authority_lifetime, push_server, outbox, server_flags, title, skip_packages):
|
def certidude_setup_authority(username, kerberos_keytab, nginx_config, country, state, locality, organization, organizational_unit, common_name, directory, authority_lifetime, push_server, outbox, server_flags, title, skip_packages, elliptic_curve):
|
||||||
assert os.getuid() == 0 and os.getgid() == 0, "Authority can be set up only by root"
|
assert os.getuid() == 0 and os.getgid() == 0, "Authority can be set up only by root"
|
||||||
|
|
||||||
import pwd
|
import pwd
|
||||||
@ -1160,9 +1171,12 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, country,
|
|||||||
|
|
||||||
# Generate and sign CA key
|
# Generate and sign CA key
|
||||||
if not os.path.exists(ca_key):
|
if not os.path.exists(ca_key):
|
||||||
|
if elliptic_curve:
|
||||||
|
click.echo("Generating %s EC key for CA ..." % const.CURVE_NAME)
|
||||||
|
public_key, private_key = asymmetric.generate_pair("ec", curve=const.CURVE_NAME)
|
||||||
|
else:
|
||||||
click.echo("Generating %d-bit RSA key for CA ..." % const.KEY_SIZE)
|
click.echo("Generating %d-bit RSA key for CA ..." % const.KEY_SIZE)
|
||||||
|
public_key, private_key = asymmetric.generate_pair("rsa", bit_size=const.KEY_SIZE)
|
||||||
public_key, private_key = asymmetric.generate_pair('rsa', bit_size=const.KEY_SIZE)
|
|
||||||
|
|
||||||
names = (
|
names = (
|
||||||
("country_name", country),
|
("country_name", country),
|
||||||
|
@ -99,3 +99,6 @@ PROFILES = OrderedDict([[i, [j.strip() for j in cp.get("profile", i).split(",")]
|
|||||||
cp2 = configparser.RawConfigParser()
|
cp2 = configparser.RawConfigParser()
|
||||||
cp2.readfp(open(const.BUILDER_CONFIG_PATH, "r"))
|
cp2.readfp(open(const.BUILDER_CONFIG_PATH, "r"))
|
||||||
IMAGE_BUILDER_PROFILES = [(j, cp2.get(j, "title"), cp2.get(j, "rename")) for j in cp2.sections()]
|
IMAGE_BUILDER_PROFILES = [(j, cp2.get(j, "title"), cp2.get(j, "rename")) for j in cp2.sections()]
|
||||||
|
|
||||||
|
|
||||||
|
TOKEN_OVERWRITE_PERMITTED=True
|
||||||
|
@ -5,6 +5,8 @@ import socket
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
KEY_SIZE = 1024 if os.getenv("TRAVIS") else 4096
|
KEY_SIZE = 1024 if os.getenv("TRAVIS") else 4096
|
||||||
|
CURVE_NAME = "secp384r1"
|
||||||
|
|
||||||
RUN_DIR = "/run/certidude"
|
RUN_DIR = "/run/certidude"
|
||||||
CONFIG_DIR = "/etc/certidude"
|
CONFIG_DIR = "/etc/certidude"
|
||||||
SERVER_CONFIG_PATH = os.path.join(CONFIG_DIR, "server.conf")
|
SERVER_CONFIG_PATH = os.path.join(CONFIG_DIR, "server.conf")
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
<div class="highlight">
|
<div class="highlight">
|
||||||
<pre><code>easy_install pip;
|
<pre><code>easy_install pip;
|
||||||
pip3 install certidude;
|
pip3 install certidude;
|
||||||
certidude bootstrap {{session.authority.common_name}}
|
certidude bootstrap {{ window.location.hostname }}
|
||||||
</code></pre>
|
</code></pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -30,14 +30,16 @@ Signature="$Windows NT$
|
|||||||
[NewRequest]
|
[NewRequest]
|
||||||
Subject = "CN=$hostname"
|
Subject = "CN=$hostname"
|
||||||
Exportable = FALSE
|
Exportable = FALSE
|
||||||
KeyLength = 2048
|
|
||||||
KeySpec = 1
|
KeySpec = 1
|
||||||
KeyUsage = 0xA0
|
KeyUsage = 0xA0
|
||||||
MachineKeySet = True
|
MachineKeySet = True
|
||||||
ProviderName = "Microsoft RSA SChannel Cryptographic Provider"
|
|
||||||
ProviderType = 12
|
ProviderType = 12
|
||||||
RequestType = PKCS10
|
RequestType = PKCS10
|
||||||
"@
|
{% if session.authority.certificate.algorithm == "ec" %}ProviderName = "Microsoft Software Key Storage Provider"
|
||||||
|
KeyAlgorithm = ECDSA_P384
|
||||||
|
{% else %}ProviderName = "Microsoft RSA SChannel Cryptographic Provider"
|
||||||
|
KeyLength = 2048
|
||||||
|
{% endif %}"@
|
||||||
|
|
||||||
$templ | Out-File req.inf
|
$templ | Out-File req.inf
|
||||||
|
|
||||||
@ -54,8 +56,23 @@ Import-Certificate -FilePath client_cert.pem -CertStoreLocation Cert:\LocalMachi
|
|||||||
|
|
||||||
# Set up IPSec VPN tunnel
|
# Set up IPSec VPN tunnel
|
||||||
Remove-VpnConnection -AllUserConnection -Force k-space
|
Remove-VpnConnection -AllUserConnection -Force k-space
|
||||||
Add-VpnConnection -Name k-space -ServerAddress guests.k-space.ee -SplitTunneling -PassThru -TunnelType ikev2 -AllUserConnection -AuthenticationMethod MachineCertificate
|
Add-VpnConnection `
|
||||||
Set-VpnConnectionIPsecConfiguration -ConnectionName k-space -AuthenticationTransformConstants SHA256128 -CipherTransformConstants AES256 -EncryptionMethod AES256 -IntegrityCheckMethod SHA384 -PfsGroup PFS24 -DHGroup Group24 -PassThru -AllUserConnection -Force</code></pre>
|
-Name k-space `
|
||||||
|
-ServerAddress guests.k-space.ee `
|
||||||
|
-AuthenticationMethod MachineCertificate `
|
||||||
|
-SplitTunneling `
|
||||||
|
-TunnelType ikev2 `
|
||||||
|
-PassThru -AllUserConnection
|
||||||
|
|
||||||
|
# Security hardening
|
||||||
|
Set-VpnConnectionIPsecConfiguration `
|
||||||
|
-ConnectionName k-space `
|
||||||
|
-AuthenticationTransformConstants GCMAES128 `
|
||||||
|
-CipherTransformConstants GCMAES128 `
|
||||||
|
-EncryptionMethod AES128 `
|
||||||
|
-IntegrityCheckMethod none `
|
||||||
|
-PfsGroup {% if session.authority.certificate.algorithm == "ec" %}ECP384 -DHGroup ECP384{% else %}PFS2048 -DHGroup Group14{% endif %} `
|
||||||
|
-PassThru -AllUserConnection -Force</code></pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h5>UNIX & UNIX-like</h5>
|
<h5>UNIX & UNIX-like</h5>
|
||||||
@ -63,58 +80,110 @@ Set-VpnConnectionIPsecConfiguration -ConnectionName k-space -AuthenticationTrans
|
|||||||
<p>On other UNIX-like machines generate key pair and submit the signing request using OpenSSL and cURL:</p>
|
<p>On other UNIX-like machines generate key pair and submit the signing request using OpenSSL and cURL:</p>
|
||||||
<div class="highlight">
|
<div class="highlight">
|
||||||
<pre class="code"><code>NAME=$(hostname);
|
<pre class="code"><code>NAME=$(hostname);
|
||||||
openssl genrsa -out client_key.pem 2048;
|
{% if session.authority.certificate.algorithm == "ec" %}openssl ecparam -name secp384r1 -genkey -noout -out client_key.pem{% else %}openssl genrsa -out client_key.pem 2048;{% endif %}
|
||||||
openssl req -new -sha256 -key client_key.pem -out client_req.pem -subj "/CN=$NAME";
|
openssl req -new -sha384 -key client_key.pem -out client_req.pem -subj "/CN=$NAME";
|
||||||
curl -f -L -H "Content-type: application/pkcs10" --data-binary @client_req.pem \
|
curl -f -L -H "Content-type: application/pkcs10" --data-binary @client_req.pem \
|
||||||
http://{{ window.location.hostname }}/api/request/?wait=yes > client_cert.pem</code></pre>
|
http://{{ window.location.hostname }}/api/request/?wait=yes > client_cert.pem</code></pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h5>OpenVPN gateway on OpenWrt/LEDE router</h5>
|
<h5>StrongSwan as client</h5>
|
||||||
|
|
||||||
|
<p>First enroll certificates:</p>
|
||||||
|
<div class="highlight">
|
||||||
|
<pre class="code"><code>
|
||||||
|
FQDN=$(cat /etc/hostname)
|
||||||
|
curl -f http://{{ window.location.hostname }}/api/certificate/ -o /etc/ipsec.d/cacerts/ca.pem; \
|
||||||
|
test -e /etc/ipsec.d/private/client.pem \
|
||||||
|
|| openssl ecparam -name secp384r1 -genkey -noout -out /etc/ipsec.d/private/client.pem; \
|
||||||
|
test -e /etc/ipsec.d/reqs/client.pem \
|
||||||
|
|| openssl req -new -sha384 \
|
||||||
|
-key /etc/ipsec.d/private/client.pem \
|
||||||
|
-out /etc/ipsec.d/reqs/client.pem -subj "/CN=$FQDN"; \
|
||||||
|
curl -f -L -H "Content-type: application/pkcs10" \
|
||||||
|
--data-binary @/etc/ipsec.d/reqs/client.pem \
|
||||||
|
-o /etc/ipsec.d/certs/client.pem \
|
||||||
|
http://{{ window.location.hostname }}/api/request/?wait=yes</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>Then configure StrongSwan</p>
|
||||||
|
<div class="highlight">
|
||||||
|
<pre class="code"><code>
|
||||||
|
cat > /etc/ipsec.conf << EOF
|
||||||
|
conn c2s
|
||||||
|
auto=start
|
||||||
|
right=router.k-space.ee
|
||||||
|
dpdaction=restart
|
||||||
|
closeaction=restart
|
||||||
|
left=%defaultroute
|
||||||
|
rightsubnet=0.0.0.0/0
|
||||||
|
keyingtries=%forever
|
||||||
|
rightid=%any
|
||||||
|
leftsourceip=%config
|
||||||
|
leftcert=client.pem
|
||||||
|
ike=aes128-sha384-{% if session.authority.certificate.algorithm == "ec" %}ecp384{% else %}modp2048{% endif %}!
|
||||||
|
esp=aes128gcm16!
|
||||||
|
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo ": {% if session.authority.certificate.algorithm == "ec" %}ECDSA{% else %}RSA{% endif %} client.pem" > /etc/ipsec.secrets
|
||||||
|
|
||||||
|
ipsec restart</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h5>OpenWrt/LEDE as VPN gateway</h5>
|
||||||
|
|
||||||
<p>First enroll certificates:</p>
|
<p>First enroll certificates:</p>
|
||||||
<div class="highlight">
|
<div class="highlight">
|
||||||
<pre class="code"><code># Derive FQDN from WAN interface's reverse DNS record
|
<pre class="code"><code># Derive FQDN from WAN interface's reverse DNS record
|
||||||
FQDN=$(nslookup $(uci get network.wan.ipaddr) | grep "name =" | head -n1 | cut -d "=" -f 2 | xargs)
|
FQDN=$(nslookup $(uci get network.wan.ipaddr) | grep "name =" | head -n1 | cut -d "=" -f 2 | xargs)
|
||||||
|
|
||||||
mkdir -p /etc/certidude/authority/{{ window.location.hostname }}; \
|
mkdir -p /etc/certidude/authority/{{ window.location.hostname }}
|
||||||
grep -c certidude /etc/sysupgrade.conf || echo /etc/certidude >> /etc/sysupgrade.conf; \
|
grep -c certidude /etc/sysupgrade.conf || echo /etc/certidude >> /etc/sysupgrade.conf
|
||||||
curl -f http://{{ window.location.hostname }}/api/certificate/ -o /etc/certidude/authority/{{ window.location.hostname }}/ca_cert.pem; \
|
cat << EOF > /etc/certidude/authority/{{ window.location.hostname }}/ca_cert.pem
|
||||||
|
{{ session.authority.certificate.blob }}
|
||||||
|
EOF
|
||||||
test -e /etc/certidude/authority/{{ window.location.hostname }}/server_key.pem \
|
test -e /etc/certidude/authority/{{ window.location.hostname }}/server_key.pem \
|
||||||
|| openssl genrsa -out /etc/certidude/authority/{{ window.location.hostname }}/server_key.pem 2048; \
|
|| openssl {% if session.authority.certificate.algorithm == "ec" %}ecparam -name secp384r1 -genkey -noout -out /etc/certidude/authority/{{ window.location.hostname }}/server_key.pem{% else %}genrsa -out /etc/certidude/authority/{{ window.location.hostname }}/server_key.pem 2048{% endif %}
|
||||||
test -e /etc/certidude/authority/{{ window.location.hostname }}/server_req.pem \
|
test -e /etc/certidude/authority/{{ window.location.hostname }}/server_req.pem \
|
||||||
|| openssl req -new -sha256 \
|
|| openssl req -new -sha384 \
|
||||||
-key /etc/certidude/authority/{{ window.location.hostname }}/server_key.pem \
|
-key /etc/certidude/authority/{{ window.location.hostname }}/server_key.pem \
|
||||||
-out /etc/certidude/authority/{{ window.location.hostname }}/server_req.pem -subj "/CN=$FQDN"; \
|
-out /etc/certidude/authority/{{ window.location.hostname }}/server_req.pem -subj "/CN=$FQDN"
|
||||||
|
cat /etc/certidude/authority/{{ window.location.hostname }}/server_req.pem
|
||||||
curl -f -L -H "Content-type: application/pkcs10" \
|
curl -f -L -H "Content-type: application/pkcs10" \
|
||||||
--data-binary @/etc/certidude/authority/{{ window.location.hostname }}/server_req.pem \
|
--data-binary @/etc/certidude/authority/{{ window.location.hostname }}/server_req.pem \
|
||||||
-o /etc/certidude/authority/{{ window.location.hostname }}/server_cert.pem \
|
-o /etc/certidude/authority/{{ window.location.hostname }}/server_cert.pem \
|
||||||
http://{{ window.location.hostname }}/api/request/?wait=yes</code></pre>
|
http://{{ window.location.hostname }}/api/request/?wait=yes
|
||||||
</div>
|
|
||||||
|
|
||||||
<p>Then set up service:</p>
|
# Create VPN gateway up/down script for reporting client IP addresses to CA
|
||||||
<div class="highlight">
|
cat <<\EOF > /etc/certidude/authority/{{ window.location.hostname }}/updown
|
||||||
<pre class="code"><code># Create VPN gateway up/down script for reporting client IP addresses to CA
|
|
||||||
cat <<\EOF > /etc/certidude/updown
|
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
CURL="curl -f --key /etc/certidude/authority/{{ window.location.hostname }}/server_key.pem --cert /etc/certidude/authority/{{ window.location.hostname }}/server_cert.pem --cacert /etc/certidude/authority/{{ window.location.hostname }}/ca_cert.pem https://{{ window.location.hostname }}:8443/api/lease/"
|
|
||||||
|
CURL="curl -m 3 -f --key /etc/certidude/authority/{{ window.location.hostname }}/server_key.pem --cert /etc/certidude/authority/{{ window.location.hostname }}/server_cert.pem --cacert /etc/certidude/authority/{{ window.location.hostname }}/ca_cert.pem https://{{ window.location.hostname }}:8443/api/lease/"
|
||||||
|
|
||||||
case $PLUTO_VERB in
|
case $PLUTO_VERB in
|
||||||
up-client|down-client) $CURL --data "outer_address=$PLUTO_PEER&inner_address=$PLUTO_PEER_SOURCEIP&client=$PLUTO_PEER_ID" ;;
|
up-client) $CURL --data-urlencode "outer_address=$PLUTO_PEER" --data-urlencode "inner_address=$PLUTO_PEER_SOURCEIP" --data-urlencode "client=$PLUTO_PEER_ID" ;;
|
||||||
*) $CURL --data "client=$X509_0_CN&outer_address=$untrusted_ip&inner_address=$ifconfig_pool_remote_ip&serial=$tls_serial_0" ;;
|
*) ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
case $script_type in
|
||||||
|
client-connect) $CURL --data-urlencode client=$X509_0_CN --data-urlencode serial=$tls_serial_0 --data-urlencode outer_address=$untrusted_ip --data-urlencode inner_address=$ifconfig_pool_remote_ip ;;
|
||||||
|
*) ;;
|
||||||
esac
|
esac
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
chmod +x /etc/certidude/updown
|
chmod +x /etc/certidude/authority/{{ window.location.hostname }}/updown</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>Then either set up OpenVPN service:</p>
|
||||||
# Generate Diffie-Hellman parameters file for OpenVPN
|
<div class="highlight">
|
||||||
|
<pre class="code"><code># Generate Diffie-Hellman parameters file for OpenVPN
|
||||||
test -e /etc/certidude/dh.pem \
|
test -e /etc/certidude/dh.pem \
|
||||||
|| openssl dhparam 2048 -out /etc/certidude/dh.pem
|
|| openssl dhparam 2048 -out /etc/certidude/dh.pem
|
||||||
|
|
||||||
# Create interface definition for tunnel
|
# Create interface definition for tunnel
|
||||||
uci set network.vpn=interface
|
uci set network.vpn=interface
|
||||||
uci set network.vpn.name='vpn'
|
uci set network.vpn.name='vpn'
|
||||||
uci set network.vpn.ifname=tun_s2c
|
uci set network.vpn.ifname=tun_s2c_udp tun_s2c_tcp
|
||||||
uci set network.vpn.proto='none'
|
uci set network.vpn.proto='none'
|
||||||
|
|
||||||
# Create zone definition for VPN interface
|
# Create zone definition for VPN interface
|
||||||
@ -133,6 +202,14 @@ uci set firewall.openvpn.dest_port=1194
|
|||||||
uci set firewall.openvpn.proto='udp'
|
uci set firewall.openvpn.proto='udp'
|
||||||
uci set firewall.openvpn.target='ACCEPT'
|
uci set firewall.openvpn.target='ACCEPT'
|
||||||
|
|
||||||
|
# Allow TCP 443 on WAN interface
|
||||||
|
uci set firewall.openvpn=rule
|
||||||
|
uci set firewall.openvpn.name='Allow OpenVPN over TCP'
|
||||||
|
uci set firewall.openvpn.src='wan'
|
||||||
|
uci set firewall.openvpn.dest_port=443
|
||||||
|
uci set firewall.openvpn.proto='tcp'
|
||||||
|
uci set firewall.openvpn.target='ACCEPT'
|
||||||
|
|
||||||
# Forward traffic from VPN to LAN
|
# Forward traffic from VPN to LAN
|
||||||
uci set firewall.c2s=forwarding
|
uci set firewall.c2s=forwarding
|
||||||
uci set firewall.c2s.src='vpn'
|
uci set firewall.c2s.src='vpn'
|
||||||
@ -142,31 +219,83 @@ uci set firewall.c2s.dest='lan'
|
|||||||
uci set dhcp.@dnsmasq[0].localservice='0'
|
uci set dhcp.@dnsmasq[0].localservice='0'
|
||||||
|
|
||||||
touch /etc/config/openvpn
|
touch /etc/config/openvpn
|
||||||
uci set openvpn.s2c=openvpn
|
|
||||||
uci set openvpn.s2c.local=$(uci get network.wan.ipaddr)
|
# Configure OpenVPN over TCP
|
||||||
uci set openvpn.s2c.script_security=2
|
uci set openvpn.s2c_tcp=openvpn
|
||||||
uci set openvpn.s2c.client_connect='/etc/certidude/updown'
|
uci set openvpn.s2c_tcp.local=$(uci get network.wan.ipaddr)
|
||||||
uci set openvpn.s2c.tls_version_min='1.2'
|
uci set openvpn.s2c_tcp.server='10.179.43.0 255.255.255.128'
|
||||||
uci set openvpn.s2c.tls_cipher='TLS-DHE-RSA-WITH-AES-256-GCM-SHA384'
|
uci set openvpn.s2c_tcp.proto='tcp-server'
|
||||||
uci set openvpn.s2c.cipher='AES-256-CBC'
|
uci set openvpn.s2c_tcp.port='443'
|
||||||
uci set openvpn.s2c.auth='SHA384'
|
uci set openvpn.s2c_tcp.dev=tun_s2c_tcp
|
||||||
uci set openvpn.s2c.dev=tun_s2c
|
|
||||||
uci set openvpn.s2c.server='10.179.43.0 255.255.255.0'
|
# Configure OpenVPN over UDP
|
||||||
uci set openvpn.s2c.key='/etc/certidude/authority/{{ window.location.hostname }}/server_key.pem'
|
uci set openvpn.s2c_udp=openvpn
|
||||||
uci set openvpn.s2c.cert='/etc/certidude/authority/{{ window.location.hostname }}/server_cert.pem'
|
uci set openvpn.s2c_udp.local=$(uci get network.wan.ipaddr)
|
||||||
uci set openvpn.s2c.ca='/etc/certidude/authority/{{ window.location.hostname }}/ca_cert.pem'
|
uci set openvpn.s2c_udp.server='10.179.43.128 255.255.255.128'
|
||||||
uci set openvpn.s2c.dh='/etc/certidude/dh.pem'
|
uci set openvpn.s2c_tcp.dev=tun_s2c_udp
|
||||||
uci set openvpn.s2c.enabled=1
|
|
||||||
uci set openvpn.s2c.comp_lzo=yes
|
for section in s2c_tcp s2c_udp; do
|
||||||
uci add_list openvpn.s2c.push="route-metric 1000"
|
|
||||||
uci add_list openvpn.s2c.push="route $(uci get network.lan.ipaddr) $(uci get network.lan.netmask)"
|
# Common paths
|
||||||
uci add_list openvpn.s2c.push="dhcp-option DNS $(uci get network.lan.ipaddr)"
|
uci set openvpn.$section.script_security=2
|
||||||
uci add_list openvpn.s2c.push="dhcp-option DOMAIN $(uci get dhcp.@dnsmasq[0].domain)"
|
uci set openvpn.$section.client_connect='/etc/certidude/updown'
|
||||||
|
uci set openvpn.$section.key='/etc/certidude/authority/{{ window.location.hostname }}/server_key.pem'
|
||||||
|
uci set openvpn.$section.cert='/etc/certidude/authority/{{ window.location.hostname }}/server_cert.pem'
|
||||||
|
uci set openvpn.$section.ca='/etc/certidude/authority/{{ window.location.hostname }}/ca_cert.pem'
|
||||||
|
uci set openvpn.$section.dh='/etc/certidude/dh.pem'
|
||||||
|
uci set openvpn.$section.enabled=1
|
||||||
|
|
||||||
|
# DNS and routes
|
||||||
|
uci add_list openvpn.$section.push="route-metric 1000"
|
||||||
|
uci add_list openvpn.$section.push="route $(uci get network.lan.ipaddr) $(uci get network.lan.netmask)"
|
||||||
|
uci add_list openvpn.$section.push="dhcp-option DNS $(uci get network.lan.ipaddr)"
|
||||||
|
uci add_list openvpn.$section.push="dhcp-option DOMAIN $(uci get dhcp.@dnsmasq[0].domain)"
|
||||||
|
|
||||||
|
# Security hardening
|
||||||
|
uci set openvpn.$section.tls_version_min='1.2'
|
||||||
|
uci set openvpn.$section.tls_cipher='TLS-{% if session.authority.certificate.algorithm == "ec" %}ECDHE-ECDSA{% else %}DHE-RSA{% endif %}-WITH-AES-128-GCM-SHA384'
|
||||||
|
uci set openvpn.$section.cipher='AES-128-GCM'
|
||||||
|
uci set openvpn.$section.auth='SHA384'
|
||||||
|
|
||||||
|
done
|
||||||
|
|
||||||
/etc/init.d/openvpn restart
|
/etc/init.d/openvpn restart
|
||||||
/etc/init.d/firewall restart</code></pre>
|
/etc/init.d/firewall restart</code></pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<p>Alternatively or additionally set up StrongSwan:</p>
|
||||||
|
<div class="highlight">
|
||||||
|
<pre class="code"><code># Generate StrongSwan config
|
||||||
|
cat > /etc/ipsec.conf << EOF
|
||||||
|
config setup
|
||||||
|
strictcrlpolicy=yes
|
||||||
|
uniqueids = yes
|
||||||
|
|
||||||
|
ca {{ window.location.hostname }}
|
||||||
|
auto=add
|
||||||
|
cacert = /etc/certidude/authority/{{ window.location.hostname }}/ca_cert.pem
|
||||||
|
crluri = http://{{ window.location.hostname }}/api/revoked
|
||||||
|
ocspuri = http://{{ window.location.hostname }}/api/ocsp/
|
||||||
|
|
||||||
|
conn s2c
|
||||||
|
auto=add
|
||||||
|
dpdaction=clear
|
||||||
|
closeaction=clear
|
||||||
|
leftdns=$(uci get network.lan.ipaddr)
|
||||||
|
rightsourceip=172.21.0.0/24
|
||||||
|
left=$(uci get network.wan.ipaddr)
|
||||||
|
leftsubnet=$(uci get network.lan.ipaddr | cut -d . -f 1-3).0/24
|
||||||
|
leftcert=/etc/certidude/authority/{{ window.location.hostname }}/server_cert.pem
|
||||||
|
ike=aes128-sha384-{% if session.authority.certificate.algorithm == "ec" %}ecp384{% else %}modp2048{% endif %}!
|
||||||
|
esp=aes128gcm16!
|
||||||
|
leftupdown=/etc/certidude/{{ window.location.hostname }}/updown
|
||||||
|
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo ": {% if session.authority.certificate.algorithm == "ec" %}ECDSA{% else %}RSA{% endif %} /etc/certidude/authority/{{ window.location.hostname }}/server_key.pem" > /etc/ipsec.secrets
|
||||||
|
|
||||||
|
ipsec restart</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% if session.authority.builder %}
|
{% if session.authority.builder %}
|
||||||
<h5>OpenWrt/LEDE image builder</h5>
|
<h5>OpenWrt/LEDE image builder</h5>
|
||||||
@ -337,6 +466,9 @@ forbidden
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
|
|
||||||
|
|
||||||
|
{% if session.authority %}
|
||||||
|
{% if session.features.token %}
|
||||||
<h1>Tokens</h1>
|
<h1>Tokens</h1>
|
||||||
|
|
||||||
<p>Tokens allow enrolling smartphones and third party devices.</p>
|
<p>Tokens allow enrolling smartphones and third party devices.</p>
|
||||||
@ -356,9 +488,8 @@ forbidden
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div id="token_qrcode"></div>
|
<div id="token_qrcode"></div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
{% if session.authority %}
|
|
||||||
<h1>Pending requests</h1>
|
<h1>Pending requests</h1>
|
||||||
|
|
||||||
<p>Use Certidude client to apply for a certificate.
|
<p>Use Certidude client to apply for a certificate.
|
||||||
|
@ -39,7 +39,7 @@ class PosixUserManager(object):
|
|||||||
_, _, _, _, gecos, _, _ = pwd.getpwnam(username)
|
_, _, _, _, gecos, _, _ = pwd.getpwnam(username)
|
||||||
gecos = gecos.split(",")
|
gecos = gecos.split(",")
|
||||||
full_name = gecos[0]
|
full_name = gecos[0]
|
||||||
mail = "%s@%s" % (username, const.DOMAIN)
|
mail = "%s@%s" % ("tteearu", "k-space.ee") # username, "k-space.ee") # const.DOMAIN)
|
||||||
if full_name and " " in full_name:
|
if full_name and " " in full_name:
|
||||||
given_name, surname = full_name.split(" ", 1)
|
given_name, surname = full_name.split(" ", 1)
|
||||||
return User(username, mail, given_name, surname)
|
return User(username, mail, given_name, surname)
|
||||||
|
37
doc/overlay/etc/hotplug.d/iface/50-certidude
Normal file → Executable file
37
doc/overlay/etc/hotplug.d/iface/50-certidude
Normal file → Executable file
@ -5,12 +5,13 @@
|
|||||||
|
|
||||||
# TODO: renewal
|
# TODO: renewal
|
||||||
|
|
||||||
|
AUTHORITY=certidude.@authority[0]
|
||||||
|
|
||||||
[ $ACTION == "ifup" ] || exit 0
|
[ $ACTION == "ifup" ] || exit 0
|
||||||
[ $INTERFACE == "wan" ] || exit 0
|
[ $INTERFACE == "$(uci get $AUTHORITY.trigger)" ] || exit 0
|
||||||
|
|
||||||
# TODO: iterate over all authorities
|
# TODO: iterate over all authorities
|
||||||
|
|
||||||
AUTHORITY=certidude.@authority[0]
|
|
||||||
URL=$(uci get $AUTHORITY.url)
|
URL=$(uci get $AUTHORITY.url)
|
||||||
GATEWAY=$(uci get $AUTHORITY.gateway)
|
GATEWAY=$(uci get $AUTHORITY.gateway)
|
||||||
|
|
||||||
@ -40,8 +41,9 @@ logger -t certidude -s "Time is now: $(date)"
|
|||||||
# If certificate file is there assume everything's set up
|
# If certificate file is there assume everything's set up
|
||||||
if [ -f $CERTIFICATE_PATH ]; then
|
if [ -f $CERTIFICATE_PATH ]; then
|
||||||
SERIAL=$(openssl x509 -in $CERTIFICATE_PATH -noout -serial | cut -d "=" -f 2 | tr [A-F] [a-f])
|
SERIAL=$(openssl x509 -in $CERTIFICATE_PATH -noout -serial | cut -d "=" -f 2 | tr [A-F] [a-f])
|
||||||
logger -t certidude -s "Certificate with serial $SERIAL already exists, attempting to bring up IPsec tunnel..."
|
logger -t certidude -s "Certificate with serial $SERIAL already exists in $CERTIFICATE_PATH, attempting to bring up VPN tunnel..."
|
||||||
ipsec up client-to-site
|
/etc/init.d/openvpn start
|
||||||
|
/etc/init.d/ipsec start
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@ -60,7 +62,7 @@ fi
|
|||||||
if [ ! -f $KEY_PATH ]; then
|
if [ ! -f $KEY_PATH ]; then
|
||||||
KEY_TEMP=$(mktemp -u)
|
KEY_TEMP=$(mktemp -u)
|
||||||
|
|
||||||
logger -t certidude -s "Generating RSA key for IPsec..."
|
logger -t certidude -s "Generating RSA key for VPN..."
|
||||||
if [ -d $GREEN_LED ]; then
|
if [ -d $GREEN_LED ]; then
|
||||||
echo 250 | tee $GREEN_LED/delay_*
|
echo 250 | tee $GREEN_LED/delay_*
|
||||||
fi
|
fi
|
||||||
@ -167,31 +169,10 @@ fi
|
|||||||
|
|
||||||
logger -t certidude -s "Certificate md5sum: $(md5sum -b $CERTIFICATE_TEMP)"
|
logger -t certidude -s "Certificate md5sum: $(md5sum -b $CERTIFICATE_TEMP)"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
###################################
|
|
||||||
### Generate /etc/ipsec.secrets ###
|
|
||||||
###################################
|
|
||||||
|
|
||||||
SECRETS_TEMP=$(mktemp -u)
|
|
||||||
|
|
||||||
for filename in /etc/ipsec.d/private/*.pem; do
|
|
||||||
echo ": RSA $filename" >> $SECRETS_TEMP
|
|
||||||
done
|
|
||||||
|
|
||||||
uci commit
|
uci commit
|
||||||
|
|
||||||
mv $SECRETS_TEMP /etc/ipsec.secrets
|
|
||||||
mv $IPSEC_TEMP /etc/ipsec.conf
|
|
||||||
mv $CERTIFICATE_TEMP $CERTIFICATE_PATH
|
mv $CERTIFICATE_TEMP $CERTIFICATE_PATH
|
||||||
|
|
||||||
# Enable services
|
|
||||||
/etc/init.d/ipsec enable
|
|
||||||
|
|
||||||
# Restart services
|
# Restart services
|
||||||
/etc/init.d/ipsec restart
|
/etc/init.d/ipsec start
|
||||||
|
/etc/init.d/openvpn start
|
||||||
sleep 2
|
|
||||||
|
|
||||||
ipsec up client-to-site
|
|
||||||
|
@ -14,18 +14,11 @@ export PS1='\u@\h:\w\$ '
|
|||||||
[ -x /usr/bin/ldd ] || ldd() { LD_TRACE_LOADED_OBJECTS=1 $*; }
|
[ -x /usr/bin/ldd ] || ldd() { LD_TRACE_LOADED_OBJECTS=1 $*; }
|
||||||
|
|
||||||
HOSTNAME=$(uci get system.@system[0].hostname)
|
HOSTNAME=$(uci get system.@system[0].hostname)
|
||||||
DOMAIN=$(uci -q get dhcp.@dnsmasq[0].domain)
|
|
||||||
|
|
||||||
if [ $? -eq 0 ]; then
|
export PS1='\[\033[01;31m\]$HOSTNAME\[\033[01;34m\] \W #\[\033[00m\] '
|
||||||
FQDN=$HOSTNAME.$DOMAIN
|
|
||||||
else
|
|
||||||
FQDN=$HOSTNAME
|
|
||||||
fi
|
|
||||||
|
|
||||||
export PS1='\[\033[01;31m\]$FQDN\[\033[01;34m\] \W #\[\033[00m\] '
|
|
||||||
case "$TERM" in
|
case "$TERM" in
|
||||||
xterm*|rxvt*)
|
xterm*|rxvt*)
|
||||||
echo -ne "\033]0;${USER}@${FQDN}:${PWD}\007"
|
echo -ne "\033]0;${USER}@${HOSTNAME}:${PWD}\007"
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
;;
|
;;
|
||||||
|
@ -1,66 +0,0 @@
|
|||||||
# Disable DHCP servers
|
|
||||||
/etc/init.d/odhcpd disable
|
|
||||||
/etc/init.d/dnsmasq disable
|
|
||||||
|
|
||||||
# Remove firewall rules since AP bridges ethernet to wireless anyway
|
|
||||||
uci delete firewall.@zone[1]
|
|
||||||
uci delete firewall.@zone[0]
|
|
||||||
uci delete firewall.@forwarding[0]
|
|
||||||
for j in $(seq 0 10); do uci delete firewall.@rule[0]; done
|
|
||||||
|
|
||||||
# Remove WAN interface
|
|
||||||
uci delete network.wan
|
|
||||||
uci delete network.wan6
|
|
||||||
|
|
||||||
# Reconfigure DHCP client for bridge over LAN and WAN ports
|
|
||||||
uci delete network.lan.ipaddr
|
|
||||||
uci delete network.lan.netmask
|
|
||||||
uci delete network.lan.ip6assign
|
|
||||||
uci delete network.globals.ula_prefix
|
|
||||||
uci delete network.@switch_vlan[1]
|
|
||||||
uci delete dhcp.@dnsmasq[0].domain
|
|
||||||
uci set network.lan.proto=dhcp
|
|
||||||
uci set network.lan.ipv6=0
|
|
||||||
uci set network.lan.ifname='eth0'
|
|
||||||
uci set network.lan.stp=1
|
|
||||||
|
|
||||||
# Radio ordering differs among models
|
|
||||||
case $(uci get wireless.radio0.hwmode) in
|
|
||||||
11a) uci rename wireless.radio0=radio5ghz;;
|
|
||||||
11g) uci rename wireless.radio0=radio2ghz;;
|
|
||||||
esac
|
|
||||||
case $(uci get wireless.radio1.hwmode) in
|
|
||||||
11a) uci rename wireless.radio1=radio5ghz;;
|
|
||||||
11g) uci rename wireless.radio1=radio2ghz;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# Reset virtual SSID-s
|
|
||||||
uci delete wireless.@wifi-iface[1]
|
|
||||||
uci delete wireless.@wifi-iface[0]
|
|
||||||
|
|
||||||
# Pseudorandomize channel selection, should work with 80MHz on 5GHz band
|
|
||||||
case $(uci get system.@system[0].hostname | md5sum) in
|
|
||||||
1*|2*|3*|4*) uci set wireless.radio2ghz.channel=1; uci set wireless.radio5ghz.channel=36 ;;
|
|
||||||
5*|6*|7*|8*) uci set wireless.radio2ghz.channel=5; uci set wireless.radio5ghz.channel=52 ;;
|
|
||||||
9*|0*|a*|b*) uci set wireless.radio2ghz.channel=9; uci set wireless.radio5ghz.channel=100 ;;
|
|
||||||
c*|d*|e*|f*) uci set wireless.radio2ghz.channel=13; uci set wireless.radio5ghz.channel=132 ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# Create bridge for guests
|
|
||||||
uci set network.guest=interface
|
|
||||||
uci set network.guest.proto='static'
|
|
||||||
uci set network.guest.address='0.0.0.0'
|
|
||||||
uci set network.guest.type='bridge'
|
|
||||||
uci set network.guest.ifname='eth0.156' # tag id 156 for guest network
|
|
||||||
uci set network.guest.ipaddr='0.0.0.0'
|
|
||||||
uci set network.guest.ipv6=0
|
|
||||||
uci set network.guest.stp=1
|
|
||||||
|
|
||||||
# Disable switch tagging and bridge all ports on TP-Link WDR3600/WDR4300
|
|
||||||
case $(cat /etc/board.json | jsonfilter -e '@["model"]["id"]') in
|
|
||||||
tl-wdr*)
|
|
||||||
uci set network.@switch[0].enable_vlan=0
|
|
||||||
uci set network.@switch_vlan[0].ports='0 1 2 3 4 5 6'
|
|
||||||
;;
|
|
||||||
*) ;;
|
|
||||||
esac
|
|
Loading…
Reference in New Issue
Block a user