From fba8f5d77650f1345cec934531a760298207f661 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Lauri=20V=C3=B5sandi?=
Date: Wed, 3 Jan 2018 22:12:02 +0000
Subject: [PATCH] Integrate LEDE image builder
---
certidude/api/__init__.py | 9 ++-
certidude/api/builder.py | 52 +++++++++++++
certidude/api/script.py | 6 +-
certidude/cli.py | 77 +++++++++++--------
certidude/config.py | 9 ++-
certidude/const.py | 3 +-
certidude/firewall.py | 15 +++-
certidude/static/views/attributes.html | 2 +-
certidude/static/views/authority.html | 12 ++-
certidude/static/views/signed.html | 47 ++++++-----
certidude/static/views/tags.html | 2 +-
certidude/templates/server/builder.conf | 31 ++++++++
certidude/templates/server/server.conf | 6 ++
doc/build-ap.sh | 58 ++++++++++++++
.../etc/hotplug.d/iface}/50-certidude | 0
doc/overlay/etc/profile | 32 ++++++++
doc/overlay/etc/uci-defaults/40-hostname | 21 +++++
doc/overlay/etc/uci-defaults/50-access-point | 66 ++++++++++++++++
18 files changed, 386 insertions(+), 62 deletions(-)
create mode 100644 certidude/api/builder.py
create mode 100644 certidude/templates/server/builder.conf
create mode 100644 doc/build-ap.sh
rename doc/{ => overlay/etc/hotplug.d/iface}/50-certidude (100%)
create mode 100644 doc/overlay/etc/profile
create mode 100644 doc/overlay/etc/uci-defaults/40-hostname
create mode 100644 doc/overlay/etc/uci-defaults/50-access-point
diff --git a/certidude/api/__init__.py b/certidude/api/__init__.py
index 9e1fc83..5a8c212 100644
--- a/certidude/api/__init__.py
+++ b/certidude/api/__init__.py
@@ -82,7 +82,7 @@ class SessionResource(object):
attributes = {}
for key in listxattr(path):
if key.startswith(b"user.machine."):
- attributes[key[13:]] = getxattr(path, key).decode("ascii")
+ attributes[key[13:].decode("ascii")] = getxattr(path, key).decode("ascii")
# Extract lease information from filesystem
try:
@@ -131,6 +131,9 @@ class SessionResource(object):
),
request_submission_allowed = config.REQUEST_SUBMISSION_ALLOWED,
authority = dict(
+ builder = dict(
+ profiles = config.IMAGE_BUILDER_PROFILES
+ ),
tagging = [dict(name=t[0], type=t[1], title=t[2]) for t in config.TAG_TYPES],
lease = dict(
offline = 600, # Seconds from last seen activity to consider lease offline, OpenVPN reneg-sec option
@@ -208,6 +211,7 @@ def certidude_app(log_handlers=[]):
from .attrib import AttributeResource
from .bootstrap import BootstrapResource
from .token import TokenResource
+ from .builder import ImageBuilderResource
app = falcon.API(middleware=NormalizeMiddleware())
app.req_options.auto_parse_form_urlencoded = True
@@ -240,6 +244,9 @@ def certidude_app(log_handlers=[]):
# Bootstrap resource
app.add_route("/api/bootstrap/", BootstrapResource())
+ # LEDE image builder resource
+ app.add_route("/api/build/{profile}/{suggested_filename}", ImageBuilderResource())
+
# Add CRL handler if we have any whitelisted subnets
if config.CRL_SUBNETS:
from .revoked import RevocationListResource
diff --git a/certidude/api/builder.py b/certidude/api/builder.py
new file mode 100644
index 0000000..61538a4
--- /dev/null
+++ b/certidude/api/builder.py
@@ -0,0 +1,52 @@
+
+import click
+import falcon
+import logging
+import os
+import subprocess
+from certidude import config, const
+from certidude.auth import login_required, authorize_admin
+from jinja2 import Template
+
+logger = logging.getLogger(__name__)
+
+class ImageBuilderResource(object):
+ @login_required
+ @authorize_admin
+ def on_get(self, req, resp, profile, suggested_filename):
+ model = config.cp2.get(profile, "model")
+ build_script_path = config.cp2.get(profile, "command")
+ overlay_path = config.cp2.get(profile, "overlay")
+ site_script_path = config.cp2.get(profile, "script")
+ suffix = config.cp2.get(profile, "filename")
+
+ build = "/var/lib/certidude/builder/" + profile
+ if not os.path.exists(build + "/overlay/etc/uci-defaults"):
+ os.makedirs(build + "/overlay/etc/uci-defaults")
+ os.system("rsync -av " + overlay_path + "/ " + build + "/overlay/")
+
+ if site_script_path:
+ template = Template(open(site_script_path).read())
+ with open(build + "/overlay/etc/uci-defaults/99-site-config", "w") as fh:
+ fh.write(template.render(authority_name=const.FQDN))
+
+ proc = subprocess.Popen(("/bin/bash", build_script_path),
+ stdout=open(build + "/build.log", "w"), stderr=subprocess.STDOUT,
+ close_fds=True, shell=False,
+ cwd=build,
+ env={"PROFILE":model, "PATH":"/usr/sbin:/usr/bin:/sbin:/bin"},
+ startupinfo=None, creationflags=0)
+ proc.communicate()
+
+ for dname in os.listdir(build):
+ if dname.startswith("lede-imagebuilder-"):
+ for root, dirs, files in os.walk(os.path.join(build, dname, "bin", "targets")):
+ for filename in files:
+ if filename.endswith(suffix):
+ path = os.path.join(root, filename)
+ click.echo("Serving: %s" % path)
+ resp.body = open(path, "rb").read()
+ resp.set_header("Content-Disposition", ("attachment; filename=%s" % suggested_filename))
+ return
+ raise falcon.HTTPNotFound()
+
diff --git a/certidude/api/script.py b/certidude/api/script.py
index 889519b..494e528 100644
--- a/certidude/api/script.py
+++ b/certidude/api/script.py
@@ -1,5 +1,6 @@
import falcon
import logging
+import os
from certidude import const, config, authority
from certidude.decorators import serialize
from jinja2 import Environment, FileSystemLoader
@@ -26,9 +27,10 @@ class ScriptResource():
except AttributeError: # No tags
pass
- script = named_tags.get("script", config.SCRIPT_DEFAULT)
+ script = named_tags.get("script", "default.sh")
+ assert script in os.listdir(config.SCRIPT_DIR)
resp.set_header("Content-Type", "text/x-shellscript")
- resp.body = env.get_template(script).render(
+ resp.body = env.get_template(os.path.join(script)).render(
authority_name=const.FQDN,
common_name=cn,
other_tags=other_tags,
diff --git a/certidude/cli.py b/certidude/cli.py
index 5b57495..ea31874 100755
--- a/certidude/cli.py
+++ b/certidude/cli.py
@@ -96,7 +96,7 @@ def setup_client(prefix="client_", dh=False):
@click.option("-s", "--skip-self", default=False, is_flag=True, help="Skip self enroll")
@click.option("-nw", "--no-wait", default=False, is_flag=True, help="Return immideately if server doesn't autosign")
def certidude_enroll(fork, renew, no_wait, kerberos, skip_self):
- if not skip_self and os.path.exists(const.CONFIG_PATH):
+ if not skip_self and os.path.exists(const.SERVER_CONFIG_PATH):
click.echo("Self-enrolling authority's web interface certificate")
from certidude import authority
authority.self_enroll()
@@ -944,38 +944,42 @@ def certidude_setup_openvpn_networkmanager(authority, remote, common_name, **pat
@click.option("--directory", help="Directory for authority files")
@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("--skip-packages", is_flag=True, help="Don't attempt to install apt/pip/npm packages")
@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):
+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):
assert os.getuid() == 0 and os.getgid() == 0, "Authority can be set up only by root"
import pwd
from jinja2 import Environment, PackageLoader
env = Environment(loader=PackageLoader("certidude", "templates"), trim_blocks=True)
- click.echo("Installing packages...")
- os.system("apt-get install -qq -y cython3 python3-dev python3-mimeparse \
- python3-markdown python3-pyxattr python3-jinja2 python3-cffi \
- software-properties-common libsasl2-modules-gssapi-mit npm nodejs \
- libkrb5-dev libldap2-dev libsasl2-dev")
- os.system("pip3 install -q --upgrade gssapi falcon humanize ipaddress simplepam")
- os.system("pip3 install -q --pre --upgrade python-ldap")
-
- if not os.path.exists("/usr/lib/nginx/modules/ngx_nchan_module.so"):
- click.echo("Enabling nginx PPA")
- os.system("add-apt-repository -y ppa:nginx/stable")
- os.system("apt-get update -q")
- os.system("apt-get install -y -q libnginx-mod-nchan")
+ if skip_packages:
+ click.echo("Not attempting to install packages from APT as requested...")
else:
- click.echo("PPA for nginx already enabled")
+ click.echo("Installing packages...")
+ os.system("apt-get install -qq -y cython3 python3-dev python3-mimeparse \
+ python3-markdown python3-pyxattr python3-jinja2 python3-cffi \
+ software-properties-common libsasl2-modules-gssapi-mit npm nodejs \
+ libkrb5-dev libldap2-dev libsasl2-dev gawk libncurses5-dev")
+ os.system("pip3 install -q --upgrade gssapi falcon humanize ipaddress simplepam")
+ os.system("pip3 install -q --pre --upgrade python-ldap")
- if not os.path.exists("/usr/sbin/nginx"):
- click.echo("Installing nginx from PPA")
- os.system("apt-get install -y -q nginx")
- else:
- click.echo("Web server nginx already installed")
+ if not os.path.exists("/usr/lib/nginx/modules/ngx_nchan_module.so"):
+ click.echo("Enabling nginx PPA")
+ os.system("add-apt-repository -y ppa:nginx/stable")
+ os.system("apt-get update -q")
+ os.system("apt-get install -y -q libnginx-mod-nchan")
+ else:
+ click.echo("PPA for nginx already enabled")
- if not os.path.exists("/usr/bin/node"):
- os.symlink("/usr/bin/nodejs", "/usr/bin/node")
+ if not os.path.exists("/usr/sbin/nginx"):
+ click.echo("Installing nginx from PPA")
+ os.system("apt-get install -y -q nginx")
+ else:
+ click.echo("Web server nginx already installed")
+
+ if not os.path.exists("/usr/bin/node"):
+ os.symlink("/usr/bin/nodejs", "/usr/bin/node")
# Generate secret for tokens
token_secret = ''.join(random.choice(string.ascii_letters + string.digits + '!@#$%^&*()') for i in range(50))
@@ -1036,6 +1040,9 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, country,
else:
click.echo("Warning: /etc/krb5.keytab or /etc/samba/smb.conf not found, Kerberos unconfigured")
+ doc_path = os.path.join(os.path.realpath(os.path.dirname(os.path.dirname(__file__))), "doc")
+ script_dir = os.path.join(os.path.realpath(os.path.dirname(__file__)), "templates", "script")
+
static_path = os.path.join(os.path.realpath(os.path.dirname(__file__)), "static")
certidude_path = sys.argv[0]
@@ -1057,6 +1064,13 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, country,
else:
click.echo("Not systemd based OS, don't know how to set up initscripts")
+ if os.path.exists("/etc/certidude/builder.conf"):
+ click.echo("Image builder config /etc/certidude/builder.conf already exists, remove to regenerate")
+ else:
+ with open("/etc/certidude/builder.conf", "w") as fh:
+ fh.write(env.get_template("server/builder.conf").render(vars()))
+ click.echo("File /etc/certidude/builder.conf created")
+
assert os.getuid() == 0 and os.getgid() == 0
bootstrap_pid = os.fork()
if not bootstrap_pid:
@@ -1069,7 +1083,10 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, country,
os.makedirs(subdir)
# Install JavaScript pacakges
- os.system("npm install --silent -g nunjucks@2.5.2 nunjucks-date@1.2.0 node-forge bootstrap@4.0.0-alpha.6 jquery timeago tether font-awesome qrcode-svg")
+ if skip_packages:
+ click.echo("Not attempting to install packages from NPM as requested...")
+ else:
+ os.system("npm install --silent -g nunjucks@2.5.2 nunjucks-date@1.2.0 node-forge bootstrap@4.0.0-alpha.6 jquery timeago tether font-awesome qrcode-svg")
# Compile nunjucks templates
cmd = 'nunjucks-precompile --include ".html$" --include ".svg" %s > %s.part' % (static_path, bundle_js)
@@ -1109,14 +1126,14 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, country,
if not os.path.exists(const.CONFIG_DIR):
click.echo("Creating %s" % const.CONFIG_DIR)
os.makedirs(const.CONFIG_DIR)
- if os.path.exists(const.CONFIG_PATH):
- click.echo("Configuration file %s already exists, remove to regenerate" % const.CONFIG_PATH)
+ if os.path.exists(const.SERVER_CONFIG_PATH):
+ click.echo("Configuration file %s already exists, remove to regenerate" % const.SERVER_CONFIG_PATH)
else:
os.umask(0o137)
push_token = "".join([random.choice(string.ascii_letters + string.digits) for j in range(0,32)])
- with open(const.CONFIG_PATH, "w") as fh:
+ with open(const.SERVER_CONFIG_PATH, "w") as fh:
fh.write(env.get_template("server/server.conf").render(vars()))
- click.echo("Generated %s" % const.CONFIG_PATH)
+ click.echo("Generated %s" % const.SERVER_CONFIG_PATH)
# Create directory with 755 permissions
os.umask(0o022)
@@ -1183,7 +1200,7 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, country,
from certidude import authority
authority.self_enroll()
assert os.getuid() == 0 and os.getgid() == 0, "Enroll contaminated environment"
- click.echo("To enable e-mail notifications install Postfix as sattelite system and set mailer address in %s" % const.CONFIG_PATH)
+ click.echo("To enable e-mail notifications install Postfix as sattelite system and set mailer address in %s" % const.SERVER_CONFIG_PATH)
click.echo()
click.echo("Use following commands to inspect the newly created files:")
click.echo()
@@ -1337,7 +1354,7 @@ def certidude_serve(port, listen, fork):
if port == 80:
click.echo("WARNING: Please run Certidude behind nginx, remote address is assumed to be forwarded by nginx!")
- click.echo("Using configuration from: %s" % const.CONFIG_PATH)
+ click.echo("Using configuration from: %s" % const.SERVER_CONFIG_PATH)
log_handlers = []
diff --git a/certidude/config.py b/certidude/config.py
index d9dab0f..8c98605 100644
--- a/certidude/config.py
+++ b/certidude/config.py
@@ -12,7 +12,7 @@ from random import choice
# Options that are parsed from config file are fetched here
cp = configparser.RawConfigParser()
-cp.readfp(codecs.open(const.CONFIG_PATH, "r", "utf8"))
+cp.readfp(open(const.SERVER_CONFIG_PATH, "r"))
AUTHENTICATION_BACKENDS = set([j for j in
cp.get("authentication", "backends").split(" ") if j]) # kerberos, pam, ldap
@@ -99,7 +99,10 @@ TOKEN_SECRET = cp.get("token", "secret").encode("ascii")
# TODO: Check if we don't have base or servers
# The API call for looking up scripts uses following directory as root
-SCRIPT_DIR = os.path.join(os.path.dirname(__file__), "templates", "script")
-SCRIPT_DEFAULT = "default.sh"
+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")])
+
+cp2 = configparser.RawConfigParser()
+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()]
diff --git a/certidude/const.py b/certidude/const.py
index 5f36510..2e406e9 100644
--- a/certidude/const.py
+++ b/certidude/const.py
@@ -7,7 +7,8 @@ import sys
KEY_SIZE = 1024 if os.getenv("TRAVIS") else 4096
RUN_DIR = "/run/certidude"
CONFIG_DIR = "/etc/certidude"
-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")
CLIENT_CONFIG_PATH = os.path.join(CONFIG_DIR, "client.conf")
SERVICES_CONFIG_PATH = os.path.join(CONFIG_DIR, "services.conf")
SERVER_PID_PATH = os.path.join(RUN_DIR, "server.pid")
diff --git a/certidude/firewall.py b/certidude/firewall.py
index 47ac4a1..6a16d49 100644
--- a/certidude/firewall.py
+++ b/certidude/firewall.py
@@ -1,6 +1,8 @@
import falcon
import logging
+import click
+from asn1crypto import pem, x509
logger = logging.getLogger("api")
@@ -50,6 +52,17 @@ def whitelist_subject(func):
except IOError:
raise falcon.HTTPNotFound()
else:
+ # First attempt to authenticate client with certificate
+ buf = req.get_header("X-SSL-CERT")
+ if buf:
+ header, _, der_bytes = pem.unarmor(buf.replace("\t", "").encode("ascii"))
+ origin_cert = x509.Certificate.load(der_bytes)
+ if origin_cert.native == cert.native:
+ click.echo("Subject authenticated using certificates")
+ return func(self, req, resp, cn, *args, **kwargs)
+
+ # For backwards compatibility check source IP address
+ # TODO: make it disableable
try:
inner_address = getxattr(path, "user.lease.inner_address").decode("ascii")
except IOError:
@@ -58,6 +71,6 @@ def whitelist_subject(func):
if req.context.get("remote_addr") != ip_address(inner_address):
raise falcon.HTTPForbidden("Forbidden", "Remote address %s mismatch" % req.context.get("remote_addr"))
else:
- return func(self, req, resp, cn, *args, **kwargs)
+ return func(self, req, resp, cn, *args, **kwargs)
return wrapped
diff --git a/certidude/static/views/attributes.html b/certidude/static/views/attributes.html
index 9423b3f..deb7fe4 100644
--- a/certidude/static/views/attributes.html
+++ b/certidude/static/views/attributes.html
@@ -1,3 +1,3 @@
{% for key, value in certificate.attributes %}
-{{ value }}
+{{ value }}
{% endfor %}
diff --git a/certidude/static/views/authority.html b/certidude/static/views/authority.html
index 058efd7..8e9a223 100644
--- a/certidude/static/views/authority.html
+++ b/certidude/static/views/authority.html
@@ -28,7 +28,7 @@ curl -f -L -H "Content-type: application/pkcs10" --data-binary @client_req.pem \
http://{{ window.location.hostname }}/api/request/?wait=yes > client_cert.pem
- OpenWrt/LEDE
+ Vanilla OpenWrt/LEDE
On OpenWrt/LEDE router to convert it into VPN gateway:
@@ -45,6 +45,16 @@ curl -f -L -H "Content-type: application/pkcs10" \
http://{{ window.location.hostname }}/api/request/?wait=yes
+ {% if session.authority.builder %}
+ OpenWrt/LEDE image builder
+ Hit a link to generate machine specific image. Note that this might take couple minutes to finish.
+
+ {% for name, title, filename in session.authority.builder.profiles %}
+ - {{ title }}
+ {% endfor %}
+
+ {% endif %}
+
SCEP
Use following as the enrollment URL: http://{{ window.location.hostname }}/cgi-bin/pkiclient.exe
diff --git a/certidude/static/views/signed.html b/certidude/static/views/signed.html
index 5c9c12c..29ef52b 100644
--- a/certidude/static/views/signed.html
+++ b/certidude/static/views/signed.html
@@ -24,11 +24,17 @@
Part of {{ certificate.organizational_unit }} organizational unit.
{% endif %}
+
{% if session.authority.tagging %}
-
+
{% include "views/tags.html" %}
-
+
{% endif %}
+
+ {% include "views/attributes.html" %}
+
+
+
-
-
-
- {% if session.authority.tagging %}
-
-
-
- {% endif %}
-
-
+
+ {% if session.authority.tagging %}
+
+
+
+ {% endif %}
+
+
+
To fetch certificate:
@@ -77,7 +82,7 @@ curl http://{{ window.location.hostname }}/api/signed/{{ certificate.common_name
curl http://{{ window.location.hostname }}/api/certificate/ > session.pem
openssl ocsp -issuer session.pem -CAfile session.pem \
-url http://{{ window.location.hostname }}/api/ocsp/ \
- -serial 0x{{ certificate.serial }}
+ -serial 0x{{ certificate.serial }}
To fetch script:
cd /var/lib/certidude/{{ window.location.hostname }}/
diff --git a/certidude/static/views/tags.html b/certidude/static/views/tags.html
index a1f54ef..247181a 100644
--- a/certidude/static/views/tags.html
+++ b/certidude/static/views/tags.html
@@ -2,5 +2,5 @@
{{ tag.value }}
+ onClick="onTagClicked(this);"> {{ tag.value }}
{% endfor %}
diff --git a/certidude/templates/server/builder.conf b/certidude/templates/server/builder.conf
new file mode 100644
index 0000000..58008b6
--- /dev/null
+++ b/certidude/templates/server/builder.conf
@@ -0,0 +1,31 @@
+[tpl-archer-c7]
+# Title shown in the UI
+title = TP-Link Archer C7 (Access Point)
+
+# Script to build the image, copy file to /etc/certidude/ and make modifications as necessary
+command = {{ doc_path }}/build-ap.sh
+
+# Path to filesystem overlay, used
+overlay = {{ doc_path }}/overlay
+
+# Site specific script to be copied to /etc/uci-defaults/99-site-script
+script =
+
+# Device/model/profile selection
+model = archer-c7-v2
+
+# File that will be picked from the bin/ folder
+filename = archer-c7-v2-squashfs-factory-eu.bin
+
+# And renamed to make it TFTP-friendly
+rename = ArcherC7v2_tp_recovery.bin
+
+[cf-e380ac]
+title = Comfast E380AC (Access Point)
+command = {{ doc_path }}/build-ap.sh
+overlay = {{ doc_path }}/overlay
+script =
+model = cf-e380ac-v2
+filename = cf-e380ac-v2-squashfs-factory.bin
+rename = firmware_auto.bin
+
diff --git a/certidude/templates/server/server.conf b/certidude/templates/server/server.conf
index 5e5e106..f11efad 100644
--- a/certidude/templates/server/server.conf
+++ b/certidude/templates/server/server.conf
@@ -198,3 +198,9 @@ default = client, 120,
srv = server, 365, Server
gw = server, 3, Gateway
ap = client, 1825, Access Point
+
+[script]
+# Path to the folder with scripts that can be served to the clients, set none to disable scripting
+path = {{ script_dir }}
+;path = /etc/certidude/script
+;path =
diff --git a/doc/build-ap.sh b/doc/build-ap.sh
new file mode 100644
index 0000000..d18c320
--- /dev/null
+++ b/doc/build-ap.sh
@@ -0,0 +1,58 @@
+#!/bin/bash
+
+set -e
+set -x
+umask 022
+
+VERSION=17.01.4
+BASENAME=lede-imagebuilder-$VERSION-ar71xx-generic.Linux-x86_64
+FILENAME=$BASENAME.tar.xz
+URL=http://downloads.lede-project.org/releases/$VERSION/targets/ar71xx/generic/$FILENAME
+
+PACKAGES="luci luci-app-commands \
+ collectd collectd-mod-conntrack collectd-mod-interface \
+ collectd-mod-iwinfo collectd-mod-load collectd-mod-memory \
+ collectd-mod-network collectd-mod-protocols collectd-mod-tcpconns \
+ collectd-mod-uptime \
+ openssl-util openvpn-openssl curl ca-certificates \
+ htop iftop tcpdump nmap nano -odhcp6c -odhcpd -dnsmasq \
+ -luci-app-firewall \
+ -pppd -luci-proto-ppp -kmod-ppp -ppp -ppp-mod-pppoe \
+ -kmod-ip6tables -ip6tables -luci-proto-ipv6 -kmod-iptunnel6 -kmod-ipsec6"
+
+
+if [ ! -e $FILENAME ]; then
+ wget -q $URL
+fi
+
+if [ ! -e $BASENAME ]; then
+ tar xf $FILENAME
+fi
+
+cd $BASENAME
+
+# Copy CA certificate
+AUTHORITY=$(hostname -f)
+CERTIDUDE_DIR=/var/lib/certidude/$AUTHORITY
+if [ -d "$CERTIDUDE_DIR" ]; then
+ mkdir -p overlay/$CERTIDUDE_DIR
+ cp $CERTIDUDE_DIR/ca_cert.pem overlay/$CERTIDUDE_DIR
+fi
+
+cat < EOF > overlay/etc/config/certidude
+
+config authority
+ option url http://$AUTHORITY
+ option authority_path /var/lib/certidude/$AUTHORITY/ca_cert.pem
+ option request_path /var/lib/certidude/$AUTHORITY/client_req.pem
+ option certificate_path /var/lib/certidude/$AUTHORITY/client_cert.pem
+ option key_path /var/lib/certidude/$AUTHORITY/client_key.pem
+ option key_type rsa
+ option key_length 1024
+ option red_led gl-connect:red:wlan
+ option green_led gl-connect:green:lan
+
+EOF
+
+make image FILES=../overlay/ PACKAGES="$PACKAGES" PROFILE="$PROFILE"
+
diff --git a/doc/50-certidude b/doc/overlay/etc/hotplug.d/iface/50-certidude
similarity index 100%
rename from doc/50-certidude
rename to doc/overlay/etc/hotplug.d/iface/50-certidude
diff --git a/doc/overlay/etc/profile b/doc/overlay/etc/profile
new file mode 100644
index 0000000..7203174
--- /dev/null
+++ b/doc/overlay/etc/profile
@@ -0,0 +1,32 @@
+#!/bin/sh
+[ -f /etc/banner ] && cat /etc/banner
+[ -e /tmp/.failsafe ] && cat /etc/banner.failsafe
+
+export PATH=/usr/bin:/usr/sbin:/bin:/sbin
+export HOME=$(grep -e "^${USER:-root}:" /etc/passwd | cut -d ":" -f 6)
+export HOME=${HOME:-/root}
+export PS1='\u@\h:\w\$ '
+
+[ -z "$KSH_VERSION" -o \! -s /etc/mkshrc ] || . /etc/mkshrc
+[ -x /bin/more ] || alias more=less
+[ -x /usr/bin/vim ] && alias vi=vim || alias vim=vi
+[ -x /usr/bin/arp ] || arp() { cat /proc/net/arp; }
+[ -x /usr/bin/ldd ] || ldd() { LD_TRACE_LOADED_OBJECTS=1 $*; }
+
+HOSTNAME=$(uci get system.@system[0].hostname)
+DOMAIN=$(uci -q get dhcp.@dnsmasq[0].domain)
+
+if [ $? -eq 0 ]; then
+ FQDN=$HOSTNAME.$DOMAIN
+else
+ FQDN=$HOSTNAME
+fi
+
+export PS1='\[\033[01;31m\]$FQDN\[\033[01;34m\] \W #\[\033[00m\] '
+case "$TERM" in
+ xterm*|rxvt*)
+ echo -ne "\033]0;${USER}@${FQDN}:${PWD}\007"
+ ;;
+ *)
+ ;;
+esac
diff --git a/doc/overlay/etc/uci-defaults/40-hostname b/doc/overlay/etc/uci-defaults/40-hostname
new file mode 100644
index 0000000..71fa2fa
--- /dev/null
+++ b/doc/overlay/etc/uci-defaults/40-hostname
@@ -0,0 +1,21 @@
+MODEL=$(cat /etc/board.json | jsonfilter -e '@["model"]["id"]')
+
+# Hostname prefix
+case $MODEL in
+ tl-*|archer-*) VENDOR=tplink ;;
+ cf-*) VENDOR=comfast ;;
+ *) VENDOR=ap ;;
+esac
+
+# Network interface with relevant MAC address
+case $MODEL in
+ tl-wdr*) NIC=wlan1 ;;
+ archer-*) NIC=eth1 ;;
+ cf-e380ac-v2) NIC=eth0 ;;
+ *) NIC=wlan0 ;;
+esac
+
+HOSTNAME=$VENDOR-$(cat /sys/class/net/$NIC/address | cut -d : -f 4- | sed -e 's/://g')
+uci set system.@system[0].hostname=$HOSTNAME
+uci set network.lan.hostname=$HOSTNAME
+
diff --git a/doc/overlay/etc/uci-defaults/50-access-point b/doc/overlay/etc/uci-defaults/50-access-point
new file mode 100644
index 0000000..a57ad51
--- /dev/null
+++ b/doc/overlay/etc/uci-defaults/50-access-point
@@ -0,0 +1,66 @@
+# 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