1
0
mirror of https://github.com/laurivosandi/certidude synced 2024-12-22 16:25:17 +00:00

Migrate signature profiles to separate config file

This commit is contained in:
Lauri Võsandi 2018-04-16 12:13:31 +00:00
parent b9aaec7fa6
commit 94e5f72566
10 changed files with 179 additions and 64 deletions

View File

@ -163,7 +163,8 @@ class SessionResource(AuthorityHandler):
admin_subnets=config.ADMIN_SUBNETS or None, admin_subnets=config.ADMIN_SUBNETS or None,
signature = dict( signature = dict(
revocation_list_lifetime=config.REVOCATION_LIST_LIFETIME, revocation_list_lifetime=config.REVOCATION_LIST_LIFETIME,
profiles = [dict(name=k, server=v[0]=="server", lifetime=v[1], organizational_unit=v[2], title=v[3]) for k,v in config.PROFILES.items()] profiles = sorted([p.serialize() for p in config.PROFILES.values()], key=lambda p:p.get("slug")),
) )
) if req.context.get("user").is_admin() else None, ) if req.context.get("user").is_admin() else None,
features=dict( features=dict(

View File

@ -10,6 +10,7 @@ from base64 import b64decode
from certidude import config, push, errors from certidude import config, push, errors
from certidude.auth import login_required, login_optional, authorize_admin from certidude.auth import login_required, login_optional, authorize_admin
from certidude.decorators import csrf_protection, MyEncoder from certidude.decorators import csrf_protection, MyEncoder
from certidude.profile import SignatureProfile
from datetime import datetime from datetime import datetime
from oscrypto import asymmetric from oscrypto import asymmetric
from oscrypto.errors import SignatureError from oscrypto.errors import SignatureError
@ -71,7 +72,8 @@ class RequestListResource(AuthorityHandler):
# Automatic enroll with Kerberos machine cerdentials # Automatic enroll with Kerberos machine cerdentials
resp.set_header("Content-Type", "application/x-pem-file") resp.set_header("Content-Type", "application/x-pem-file")
cert, resp.body = self.authority._sign(csr, body, overwrite=True) cert, resp.body = self.authority._sign(csr, body,
profile=config.PROFILES["rw"], overwrite=True)
logger.info("Automatically enrolled Kerberos authenticated machine %s from %s", logger.info("Automatically enrolled Kerberos authenticated machine %s from %s",
machine, req.context.get("remote_addr")) machine, req.context.get("remote_addr"))
return return
@ -89,21 +91,19 @@ class RequestListResource(AuthorityHandler):
cert_pk = cert["tbs_certificate"]["subject_public_key_info"].native cert_pk = cert["tbs_certificate"]["subject_public_key_info"].native
csr_pk = csr["certification_request_info"]["subject_pk_info"].native csr_pk = csr["certification_request_info"]["subject_pk_info"].native
try:
buf = req.get_header("X-SSL-CERT")
header, _, der_bytes = pem.unarmor(buf.replace("\t", "").encode("ascii"))
handshake_cert = x509.Certificate.load(der_bytes)
except:
raise
else:
# Same public key # Same public key
if cert_pk == csr_pk: if cert_pk == csr_pk:
buf = req.get_header("X-SSL-CERT")
# Used mutually authenticated TLS handshake, assume renewal # Used mutually authenticated TLS handshake, assume renewal
if buf:
header, _, der_bytes = pem.unarmor(buf.replace("\t", "").encode("ascii"))
handshake_cert = x509.Certificate.load(der_bytes)
if handshake_cert.native == cert.native: if handshake_cert.native == cert.native:
for subnet in config.RENEWAL_SUBNETS: for subnet in config.RENEWAL_SUBNETS:
if req.context.get("remote_addr") in subnet: if req.context.get("remote_addr") in subnet:
resp.set_header("Content-Type", "application/x-x509-user-cert") resp.set_header("Content-Type", "application/x-x509-user-cert")
_, resp.body = self.authority._sign(csr, body, overwrite=True) _, resp.body = self.authority._sign(csr, body, overwrite=True,
profile=SignatureProfile.from_cert(cert))
logger.info("Renewing certificate for %s as %s is whitelisted", common_name, req.context.get("remote_addr")) logger.info("Renewing certificate for %s as %s is whitelisted", common_name, req.context.get("remote_addr"))
return return
@ -123,7 +123,7 @@ class RequestListResource(AuthorityHandler):
if req.context.get("remote_addr") in subnet: if req.context.get("remote_addr") in subnet:
try: try:
resp.set_header("Content-Type", "application/x-pem-file") resp.set_header("Content-Type", "application/x-pem-file")
_, resp.body = self.authority._sign(csr, body) _, resp.body = self.authority._sign(csr, body, profile=config.PROFILES["rw"])
logger.info("Autosigned %s as %s is whitelisted", common_name, req.context.get("remote_addr")) logger.info("Autosigned %s as %s is whitelisted", common_name, req.context.get("remote_addr"))
return return
except EnvironmentError: except EnvironmentError:
@ -222,7 +222,7 @@ class RequestDetailResource(AuthorityHandler):
""" """
try: try:
cert, buf = self.authority.sign(cn, cert, buf = self.authority.sign(cn,
profile=req.get_param("profile", default="default"), profile=config.PROFILES[req.get_param("profile", default="rw")],
overwrite=True, overwrite=True,
signer=req.context.get("user").name) signer=req.context.get("user").name)
# Mailing and long poll publishing implemented in the function above # Mailing and long poll publishing implemented in the function above

View File

@ -71,7 +71,7 @@ def self_enroll():
from certidude import authority from certidude import authority
from certidude.common import drop_privileges from certidude.common import drop_privileges
drop_privileges() drop_privileges()
authority.sign(common_name, skip_push=True, overwrite=True, profile="srv") authority.sign(common_name, skip_push=True, overwrite=True, profile=config.PROFILES["srv"])
sys.exit(0) sys.exit(0)
else: else:
os.waitpid(pid, 0) os.waitpid(pid, 0)
@ -82,7 +82,7 @@ def self_enroll():
def get_request(common_name): def get_request(common_name):
if not re.match(const.RE_HOSTNAME, common_name): if not re.match(const.RE_COMMON_NAME, common_name):
raise ValueError("Invalid common name %s" % repr(common_name)) raise ValueError("Invalid common name %s" % repr(common_name))
path = os.path.join(config.REQUESTS_DIR, common_name + ".pem") path = os.path.join(config.REQUESTS_DIR, common_name + ".pem")
try: try:
@ -95,7 +95,7 @@ def get_request(common_name):
raise errors.RequestDoesNotExist("Certificate signing request file %s does not exist" % path) raise errors.RequestDoesNotExist("Certificate signing request file %s does not exist" % path)
def get_signed(common_name): def get_signed(common_name):
if not re.match(const.RE_HOSTNAME, common_name): if not re.match(const.RE_COMMON_NAME, common_name):
raise ValueError("Invalid common name %s" % repr(common_name)) raise ValueError("Invalid common name %s" % repr(common_name))
path = os.path.join(config.SIGNED_DIR, common_name + ".pem") path = os.path.join(config.SIGNED_DIR, common_name + ".pem")
with open(path, "rb") as fh: with open(path, "rb") as fh:
@ -158,7 +158,7 @@ def store_request(buf, overwrite=False, address="", user=""):
common_name = csr["certification_request_info"]["subject"].native["common_name"] common_name = csr["certification_request_info"]["subject"].native["common_name"]
if not re.match(const.RE_HOSTNAME, common_name): if not re.match(const.RE_COMMON_NAME, common_name):
raise ValueError("Invalid common name") raise ValueError("Invalid common name")
request_path = os.path.join(config.REQUESTS_DIR, common_name + ".pem") request_path = os.path.join(config.REQUESTS_DIR, common_name + ".pem")
@ -296,7 +296,7 @@ def export_crl(pem=True):
def delete_request(common_name): def delete_request(common_name):
# Validate CN # Validate CN
if not re.match(const.RE_HOSTNAME, common_name): if not re.match(const.RE_COMMON_NAME, common_name):
raise ValueError("Invalid common name") raise ValueError("Invalid common name")
path, buf, csr, submitted = get_request(common_name) path, buf, csr, submitted = get_request(common_name)
@ -310,7 +310,7 @@ def delete_request(common_name):
config.LONG_POLL_PUBLISH % hashlib.sha256(buf).hexdigest(), config.LONG_POLL_PUBLISH % hashlib.sha256(buf).hexdigest(),
headers={"User-Agent": "Certidude API"}) headers={"User-Agent": "Certidude API"})
def sign(common_name, skip_notify=False, skip_push=False, overwrite=False, profile="default", signer=None): def sign(common_name, profile, skip_notify=False, skip_push=False, overwrite=False, signer=None):
""" """
Sign certificate signing request by it's common name Sign certificate signing request by it's common name
""" """
@ -323,16 +323,13 @@ def sign(common_name, skip_notify=False, skip_push=False, overwrite=False, profi
# Sign with function below # Sign with function below
cert, buf = _sign(csr, csr_buf, skip_notify, skip_push, overwrite, profile, signer) cert, buf = _sign(csr, csr_buf, profile, skip_notify, skip_push, overwrite, signer)
os.unlink(req_path) os.unlink(req_path)
return cert, buf return cert, buf
def _sign(csr, buf, skip_notify=False, skip_push=False, overwrite=False, profile="default", signer=None): def _sign(csr, buf, profile, skip_notify=False, skip_push=False, overwrite=False, signer=None):
# TODO: CRLDistributionPoints, OCSP URL, Certificate URL # TODO: CRLDistributionPoints, OCSP URL, Certificate URL
if profile not in config.PROFILES:
raise ValueError("Invalid profile supplied '%s'" % profile)
assert buf.startswith(b"-----BEGIN ") assert buf.startswith(b"-----BEGIN ")
assert isinstance(csr, CertificationRequest) assert isinstance(csr, CertificationRequest)
csr_pubkey = asymmetric.load_public_key(csr["certification_request_info"]["subject_pk_info"]) csr_pubkey = asymmetric.load_public_key(csr["certification_request_info"]["subject_pk_info"])
@ -370,10 +367,9 @@ def _sign(csr, buf, skip_notify=False, skip_push=False, overwrite=False, profile
else: else:
raise FileExistsError("Will not overwrite existing certificate") raise FileExistsError("Will not overwrite existing certificate")
# Sign via signer process
dn = {u'common_name': common_name } dn = {u'common_name': common_name }
profile_server_flags, lifetime, dn["organizational_unit_name"], _ = config.PROFILES[profile] if profile.ou:
lifetime = int(lifetime) dn["organizational_unit_name"] = profile.ou
builder = CertificateBuilder(dn, csr_pubkey) builder = CertificateBuilder(dn, csr_pubkey)
builder.serial_number = random.randint( builder.serial_number = random.randint(
@ -382,18 +378,12 @@ def _sign(csr, buf, skip_notify=False, skip_push=False, overwrite=False, profile
now = datetime.utcnow() now = datetime.utcnow()
builder.begin_date = now - timedelta(minutes=5) builder.begin_date = now - timedelta(minutes=5)
builder.end_date = now + timedelta(days=lifetime) builder.end_date = now + timedelta(days=profile.lifetime)
builder.issuer = certificate builder.issuer = certificate
builder.ca = False builder.ca = profile.ca
builder.key_usage = set(["digital_signature", "key_encipherment"]) builder.key_usage = profile.key_usage
builder.extended_key_usage = profile.extended_key_usage
# If we have FQDN and profile suggests server flags, enable them builder.subject_alt_domains = [common_name]
if server_flags(common_name) and profile_server_flags:
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"])
else:
builder.subject_alt_domains = [common_name] # iOS demands SAN also for clients
builder.extended_key_usage = set(["client_auth"])
end_entity_cert = builder.build(private_key) end_entity_cert = builder.build(private_key)
end_entity_cert_buf = asymmetric.dump_certificate(end_entity_cert) end_entity_cert_buf = asymmetric.dump_certificate(end_entity_cert)

View File

@ -281,8 +281,8 @@ def certidude_enroll(fork, renew, no_wait, kerberos, skip_self):
common_name = const.FQDN common_name = const.FQDN
elif "$" in common_name: elif "$" in common_name:
raise ValueError("Invalid variable '%s' supplied, only $HOSTNAME and $FQDN allowed" % common_name) raise ValueError("Invalid variable '%s' supplied, only $HOSTNAME and $FQDN allowed" % common_name)
if not re.match(const.RE_HOSTNAME, common_name): if not re.match(const.RE_COMMON_NAME, common_name):
raise ValueError("Invalid common name '%s' supplied" % common_name) raise ValueError("Supplied common name %s doesn't match the expression %s" % (common_name, const.RE_COMMON_NAME))
################################ ################################
@ -338,7 +338,7 @@ def certidude_enroll(fork, renew, no_wait, kerberos, skip_self):
try: try:
renewal_overlap = clients.getint(authority_name, "renewal overlap") renewal_overlap = clients.getint(authority_name, "renewal overlap")
except NoOptionError: # Renewal not specified in config except NoOptionError: # Renewal not configured
renewal_overlap = None renewal_overlap = None
try: try:
@ -1169,6 +1169,14 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, country,
fh.write(env.get_template("server/builder.conf").render(vars())) fh.write(env.get_template("server/builder.conf").render(vars()))
click.echo("File %s created" % const.BUILDER_CONFIG_PATH) click.echo("File %s created" % const.BUILDER_CONFIG_PATH)
# Create image builder config
if os.path.exists(const.PROFILE_CONFIG_PATH):
click.echo("Signature profile config %s already exists, remove to regenerate" % const.PROFILE_CONFIG_PATH)
else:
with open(const.PROFILE_CONFIG_PATH, "w") as fh:
fh.write(env.get_template("server/profile.conf").render(vars()))
click.echo("File %s created" % const.PROFILE_CONFIG_PATH)
# Create directory with 755 permissions # Create directory with 755 permissions
os.umask(0o022) os.umask(0o022)
if not os.path.exists(directory): if not os.path.exists(directory):
@ -1347,12 +1355,12 @@ def certidude_list(verbose, show_key_type, show_extensions, show_path, show_sign
@click.command("sign", help="Sign certificate") @click.command("sign", help="Sign certificate")
@click.argument("common_name") @click.argument("common_name")
@click.option("--profile", "-p", default="default", help="Profile") @click.option("--profile", "-p", default="rw", help="Profile")
@click.option("--overwrite", "-o", default=False, is_flag=True, help="Revoke valid certificate with same CN") @click.option("--overwrite", "-o", default=False, is_flag=True, help="Revoke valid certificate with same CN")
def certidude_sign(common_name, overwrite, profile): def certidude_sign(common_name, overwrite, profile):
from certidude import authority from certidude import authority
drop_privileges() drop_privileges()
cert = authority.sign(common_name, overwrite=overwrite, profile=profile) cert = authority.sign(common_name, overwrite=overwrite, profile=config.PROFILES[profile])
@click.command("revoke", help="Revoke certificate") @click.command("revoke", help="Revoke certificate")
@ -1397,6 +1405,11 @@ def certidude_serve(port, listen, fork):
from certidude import config from certidude import config
click.echo("Loading signature profiles:")
for profile in config.PROFILES.values():
click.echo("- %s" % profile)
click.echo()
# Rebuild reverse mapping # Rebuild reverse mapping
for cn, path, buf, cert, signed, expires in authority.list_signed(): for cn, path, buf, cert, signed, expires in authority.list_signed():
by_serial = os.path.join(config.SIGNED_BY_SERIAL_DIR, "%x.pem" % cert.serial_number) by_serial = os.path.join(config.SIGNED_BY_SERIAL_DIR, "%x.pem" % cert.serial_number)

View File

@ -2,7 +2,9 @@ import configparser
import ipaddress import ipaddress
import os import os
from certidude import const from certidude import const
from certidude.profile import SignatureProfile
from collections import OrderedDict from collections import OrderedDict
from datetime import timedelta
# Options that are parsed from config file are fetched here # Options that are parsed from config file are fetched here
@ -96,11 +98,22 @@ TOKEN_SECRET = cp.get("token", "secret").encode("ascii")
# The API call for looking up scripts uses following directory as root # The API call for looking up scripts uses following directory as root
SCRIPT_DIR = cp.get("script", "path") SCRIPT_DIR = cp.get("script", "path")
PROFILES = OrderedDict([[i, [j.strip() for j in cp.get("profile", i).split(",")]] for i in cp.options("profile")]) from configparser import ConfigParser
profile_config = ConfigParser()
profile_config.readfp(open(const.PROFILE_CONFIG_PATH))
PROFILES = dict([(key, SignatureProfile(key,
profile_config.get(key, "title"),
profile_config.get(key, "ou"),
profile_config.getboolean(key, "ca"),
profile_config.getint(key, "lifetime"),
profile_config.get(key, "key usage"),
profile_config.get(key, "extended key usage"),
profile_config.get(key, "common name"),
)) for key in profile_config.sections()])
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 TOKEN_OVERWRITE_PERMITTED=True

View File

@ -6,12 +6,15 @@ import sys
KEY_SIZE = 1024 if os.getenv("TRAVIS") else 4096 KEY_SIZE = 1024 if os.getenv("TRAVIS") else 4096
CURVE_NAME = "secp384r1" CURVE_NAME = "secp384r1"
RE_HOSTNAME = "^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])(@(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9]))?$" RE_FQDN = "^(([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])\.)+([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])?$"
RE_HOSTNAME = "^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$"
RE_COMMON_NAME = "^[A-Za-z0-9\-\.\@]+$"
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")
BUILDER_CONFIG_PATH = os.path.join(CONFIG_DIR, "builder.conf") BUILDER_CONFIG_PATH = os.path.join(CONFIG_DIR, "builder.conf")
PROFILE_CONFIG_PATH = os.path.join(CONFIG_DIR, "profile.conf")
CLIENT_CONFIG_PATH = os.path.join(CONFIG_DIR, "client.conf") CLIENT_CONFIG_PATH = os.path.join(CONFIG_DIR, "client.conf")
SERVICES_CONFIG_PATH = os.path.join(CONFIG_DIR, "services.conf") SERVICES_CONFIG_PATH = os.path.join(CONFIG_DIR, "services.conf")
SERVER_PID_PATH = os.path.join(RUN_DIR, "server.pid") SERVER_PID_PATH = os.path.join(RUN_DIR, "server.pid")

52
certidude/profile.py Normal file
View File

@ -0,0 +1,52 @@
import click
from datetime import timedelta
from certidude import const
class SignatureProfile(object):
def __init__(self, slug, title, ou, ca, lifetime, key_usage, extended_key_usage, common_name):
self.slug = slug
self.title = title
self.ou = ou or None
self.ca = ca
self.lifetime = lifetime
self.key_usage = set(key_usage.split(" ")) if key_usage else set()
self.extended_key_usage = set(extended_key_usage.split(" ")) if extended_key_usage else set()
if common_name.startswith("^"):
self.common_name = common_name
elif common_name == "RE_HOSTNAME":
self.common_name = const.RE_HOSTNAME
elif common_name == "RE_FQDN":
self.common_name = const.RE_FQDN
elif common_name == "RE_COMMON_NAME":
self.common_name = const.RE_COMMON_NAME
else:
raise ValueError("Invalid common name constraint %s" % common_name)
@classmethod
def from_cert(self, cert):
"""
Derive signature profile from an already signed certificate, eg for renewal
"""
lifetime = cert["tbs_certificate"]["validity"]["not_after"].native.replace(tzinfo=None) - \
cert["tbs_certificate"]["validity"]["not_before"].native.replace(tzinfo=None)
return SignatureProfile(
None, "Renewal", cert.subject.native.get("organizational_unit_name"),
cert.ca, lifetime.days,
" ".join(cert.key_usage_value.native),
" ".join(cert.extended_key_usage_value.native), "^")
def serialize(self):
return dict([(key, getattr(self,key)) for key in (
"slug", "title", "ou", "ca", "lifetime", "key_usage", "extended_key_usage", "common_name")])
def __repr__(self):
bits = []
if self.lifetime >= 365:
bits.append("%d years" % (self.lifetime / 365))
if self.lifetime % 365:
bits.append("%d days" % (self.lifetime % 365))
return "%s (title=%s, ca=%s, ou=%s, lifetime=%s, key_usage=%s, extended_key_usage=%s, common_name=%s)" % (
self.slug, self.title, self.ca, self.ou, " ".join(bits), self.key_usage, self.extended_key_usage, self.common_name)

View File

@ -30,11 +30,10 @@
</button> </button>
<div class="dropdown-menu"> <div class="dropdown-menu">
{% for p in session.authority.signature.profiles %} {% for p in session.authority.signature.profiles %}
<a class="dropdown-item{% if p.server and not request.server %} disabled{% endif %}" <a class="dropdown-item{% if not request.common_name.match(p.common_name) %} disabled{% endif %}"
{% if p.server and not request.server %}title="Resubmit with FQDN as common name"{% endif %} {% if not request.common_name.match(p.common_name) %} title="Common name doesn't match expression {{ p.common_name }}"{% endif %}
href="#" onclick="javascript:$.ajax({url:'/api/request/{{request.common_name}}/?sha256sum={{ request.sha256sum }}&profile={{ p.name }}',type:'post'});"> href="#" onclick="javascript:$.ajax({url:'/api/request/{{request.common_name}}/?sha256sum={{ request.sha256sum }}&profile={{ p.slug }}',type:'post'});">
{% if p.title %}{{ p.title }} ({% if p.server %}server{% else %}client{% endif %}){% else %} {{ p.title }}, expires in {{ p.lifetime }} days</a>
{% if p.server %}Server{% else %}Client{% endif %}{% endif %}, expires in {{ p.lifetime }} days</a>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>

View File

@ -0,0 +1,52 @@
[DEFAULT]
ou =
lifetime = 120
ca = false
common name = RE_COMMON_NAME
key usage = digital_signature key_encipherment
extended key usage =
[ca]
title = Certificate Authority
common name = ^ca
ca = true
key usage = key_cert_sign crl_sign
extended key usage =
lifetime = 1095
[rw]
title = Roadwarrior
ou = Roadwarrior
common name = RE_HOSTNAME
extended key usage = client_auth
[srv]
title = Server
;ou = Server
common name = RE_FQDN
lifetime = 365
extended key usage = server_auth
[gw]
title = Gateway
ou = Gateway
common name = RE_FQDN
renewable = true
lifetime = 30
extended key usage = server_auth 1.3.6.1.5.5.8.2.2 client_auth
[ap]
title = Access Point
ou = Access Point
common name = RE_HOSTNAME
lifetime = 1825
extended key usage = client_auth
[mfp]
title = Printers
ou = Printers
common name = ^mfp\-
lifetime = 30
extended key usage = client_auth

View File

@ -194,14 +194,6 @@ lifetime = 30
# Secret for generating and validating tokens, regenerate occasionally # Secret for generating and validating tokens, regenerate occasionally
secret = {{ token_secret }} secret = {{ token_secret }}
[profile]
# name, flags, lifetime, organizational unit, title
default = client, 120, Roadwarrior, Roadwarrior
gw = server, 30, Gateway, Gateway
srv = server, 365, Server,
ap = client, 1825, Access Point, Access Point
mfp = client, 30, MFP, Printers
[script] [script]
# Path to the folder with scripts that can be served to the clients, set none to disable scripting # Path to the folder with scripts that can be served to the clients, set none to disable scripting
path = {{ script_dir }} path = {{ script_dir }}