SnappyConfinement

Differences between revisions 22 and 57 (spanning 35 versions)
Revision 22 as of 2015-03-23 14:27:43
Size: 18004
Editor: jdstrand
Comment:
Revision 57 as of 2016-11-17 16:29:21
Size: 4688
Editor: jdstrand
Comment:
Deletions are marked like this. Additions are marked like this.
Line 5: Line 5:
 * '''Contributors''': [[LaunchpadHome:jdstrand|Jamie Strandboge]], [[LaunchpadHome:tyhicks|Tyler Hicks]], [[LaunchpadHome:mdeslaur|Marc Deslauriers]]
 * '''Packages affected''': apparmor, apparmor-easyprof-ubuntu-snappy, click-apparmor, snappy
 * '''Status''': Alpha
 * '''Contributors''': [[LaunchpadHome:jdstrand|Jamie Strandboge]], [[LaunchpadHome:tyhicks|Tyler Hicks]]
 * '''Packages affected''': apparmor, libseccomp, ubuntu-core-security, ubuntu-snappy, ubuntu-core-launcher (historically, click-apparmor)
 * '''Status''': Production
Line 10: Line 10:
Snappy confinement is an evolution of the [[https://wiki.ubuntu.com/SecurityTeam/Specifications/ApplicationConfinement|security model for Ubuntu Touch]]. The basic concepts for confined applications and the !AppStore model pertain to snappy applications as well. In short, applications are confined using AppArmor by default and this is achieved through a simple template-based system where policy is extended through the use of policy groups. Snappy confinement is an evolution of the [[https://wiki.ubuntu.com/SecurityTeam/Specifications/ApplicationConfinement|security model for Ubuntu Touch]]. The basic concepts for confined applications and the !AppStore model pertain to snappy applications as well. In short, applications are confined by default through the use of various technologies and this is achieved through a simple template-based system where policy is extended through the use of interfaces.
Line 12: Line 12:
It will be most helpful if you are familiar with:
 * http://www.ubuntu.com/snappy#core-tour
 * http://developer.ubuntu.com/snappy/filesystem-layout/
 * http://developer.ubuntu.com/snappy/packaging-format-for-apps/
Please see the security whitepaper for the most up to date information on Ubuntu Core series 16: https://developer.ubuntu.com/en/snappy/guides/security-whitepaper/ (starting at section 'Snappy for Ubuntu Core')
Line 17: Line 14:
== Implementation summary ==
Snappy uses a simple packaging format that is an evolution of [[https://wiki.ubuntu.com/SecurityTeam/Specifications/ApplicationConfinement/Manifest|click packaging]]. Snappy packages use a declarative [[https://developer.ubuntu.com/en/snappy/guides/packaging-format-apps/|yaml syntax]] and defaults to using `default` !AppArmor template and the "networking" policy group. Apps will be able to customize the default behavior in a number of ways via the yaml syntax.
Ubuntu Core 15.04 spec can be viewed here: https://wiki.ubuntu.com/SecurityTeam/Specifications/SnappyConfinement15.04
Line 20: Line 16:
=== Native snap format (DRAFT) ===
'''IMPORTANT''': the subsections for native snap format are currently being implemented. Please see 'Transitional click format' (below) for information on security policy until this is implemented.
The most up to date information on snappy confinement can be found here: https://github.com/snapcore/snapd/wiki/Security
Line 23: Line 18:
==== Security policy ====
Security policy in snappy will be by using a number of technologies including `apparmor`, `seccomp`, `cgroups`, etc. Internally, the security policy is generated based on what is in the snappy packaging yaml. Applications are tracked by the system by using the concept of an ApplicationId. The APP_ID is the composition of the package name, the service/binary name and package version. The APP_ID takes the form of `<pkgname>_<appname>_<version>`. For example, if this is in packaging yaml:{{{
name: foo
version: 0.1
== Debugging ==
When debugging policy issues, the `snappy-debug.security` tool can help. Use `sudo snap install snappy-debug` and then simply launch it to have it follow the logs and provide suggestions:{{{
$ sudo snap install snappy-debug
$ sudo snap connect snappy-debug:log-observe ubuntu-core:log-observe
$ sudo /snap/bin/snappy-debug.security scanlog
Line 28: Line 24:
services:
  - name: bar
    start: bin/bar
Line 33: Line 26:
Then the APP_ID for the 'bar' service is `foo_bar_0.1`. Notice, this is '<name>/<basename of binary/service name>'). The APP_ID is used throughout the system including in the enforcement of security policy. `snappy-debug.security scanlog` will report both !AppArmor and seccomp denials.
Line 35: Line 28:
A launcher will be implemented that will:
 * setup cgroups (tag network traffic, block devices, memory*)
 * setup iptables (for internal app access)
 * drop privileges to uid of service
 * setup seccomp
 * setup various environment variables (eg, SNAP_APP_PATH, SNAP_APP_DATA_PATH, SNAP_APP_USER_DATA_PATH, SNAP_APP_TMPDIR, SNAP_OLD_PWD, SNAP_APP_ARCH and HOME).
 * chdir to SNAP_APP_PATH (the install directory)
 * exec the app under apparmor profile under default nice value

The launcher will be used when lauching both services and using CLI binaries. The default policy allows ELF executables, python, perl and shell (with selected corresponding utilities from /bin and /usr/bin), disallows capabilities(7) and enforces application isolation as per the [[https://developer.ubuntu.com/en/snappy/guides/filesystem-layout/|snappy FHS]].


===== AppArmor =====
Upon snap package install, the yaml is examined and `apparmor` profiles are generated for each service and binary and have names based on the APP_ID. `apparmor` profiles are template based and may be extended through policy groups, which are expressed in the yaml as `caps`.
 * If unspecified in the packaging yaml, `snappy` will choose the `default` template and `networking` policy group (this may change as snappy involves)
 * Apps may choose to specify an alternate confinement for binaries and services by specifying `caps` and/or `security-template` in the yaml. Eg:{{{
...
services:
  - name: bar
    start: bin/bar
    caps: networking
  - name: baz
    start: bin/baz
    security-template: nondefault
    caps: networking, something, etc
Alternatively you can use the lowlevel tools to check to see if you have any denials:{{{
$ sudo journalctl --no-pager -k | grep audit
Line 62: Line 32:
`apparmor` policy templates and policy groups are shipped on the snappy system (eg, the `apparmor-easyprof-ubuntu-snappy` package) and also via framework policy snaps. Apps that depend on a particular framework may reference the framework policy snap's templates and/or groups. Importantly, frameworks are shipped (and therefore controlled) separately from the framework policy.

Apps may also optionally specify `security-override` to specify high level overrides to use when `security-template’ and `caps’ are not sufficient. The path specified by `security-override` is a custom [[https://wiki.ubuntu.com/SecurityTeam/Specifications/ApplicationConfinement/Manifest|security manifest]]. Use of this will trigger manual review in the Ubuntu store. Eg:{{{
services:
  - name: bar
    start: bin/bar
    caps: networking
  - name: baz
    start: bin/baz
    security-overrides:
      apparmor: path/to/security override
}}}

Furthermore, apps may also specify `security-policy` instead of using the template based policy to use hand-crafted `apparmor` policy. Use of this will trigger a manual review in the Ubuntu store. Eg:{{{
services:
  - name: bar
    start: bin/bar
    caps: networking
  - name: baz
    start: bin/baz
    security-policy:
      apparmor: meta/apparmor.profile
}}}

===== Seccomp =====
Like with `apparmor` (see above), on snap package install the yaml is examined and seccomp filter lists are generated for each service and binary. These seccomp filter lists are template based and may be extended through filter groups, which are expressed in the yaml as `caps`. For simplicity, the seccomp filter groups and templates will have the same names as the corresponding `apparmor` policy groups and templates such that:
 * If unspecified in the packaging yaml, `snappy` will choose the `default` filter template and `networking` filter group (this may change as snappy involves)
 * Apps may choose to specify an alternate confinement for binaries and services by specifying `caps` and/or `security-template`

Apps may also optionally specify `security-override` to specify high level overrides to use when `security-template` and `caps` are not sufficient. The path specified by `security-override` is a custom seccomp filter manifest. Use of this will trigger manual review in the Ubuntu store. Eg:{{{
services:
  - name: bar
    start: bin/bar
    caps: networking
  - name: baz
    start: bin/baz
    security-overrides:
      seccomp: path/to/filter override
}}}

Furthermore, apps may also specify `security-policy` instead of using the template based policy to use hand-crafted seccomp filters. Use of this will trigger a manual review in the Ubuntu store. Eg:{{{
services:
  - name: bar
    start: bin/bar
    caps: networking
  - name: baz
    start: bin/baz
    security-policy:
      seccomp: meta/seccomp-filter.list
}}}

===== Cgroups (DRAFT) =====
Certain cgroups will be setup per app and there will be a mapping from the APP_ID to the cgroup id. This will be used to for example tag network traffic.

===== Privilege dropping =====
TODO: a mechanism will be provided for apps to drop privileges


=== Transitional click format ===
At first, under the hood the `snappy build` command will initially create a click format package using the [[https://developer.ubuntu.com/en/snappy/guides/packaging-format-apps/|yaml syntax]]. Eventually `snappy build` will create a native snap formatted package (see above) and not use the click format. This section describes the transition implementation.

Upon package install:
 * the security policy is generated (running any click hooks, such as `aa-clickhook`)
 * launcher preparation is performed:
  * for services, a systemd service file is generated from the yaml (initially, `AppArmorProfile=<profilename>` is added to this service file, but eventually the launcher will be used)
  * for cli binaries, the binary will be registered with the system and added to the user's PATH (initially, a shell wrapper script is created in /apps/bin for each cli binary)

==== apparmor hook ====
Under the hood, the [[https://wiki.ubuntu.com/SecurityTeam/Specifications/ApplicationConfinement/Manifest|security manifest]] is used to describe the `apparmor` confinement of the app. Most apps do not need to specify anything for confinement and snappy will create a security manifest like the following:{{{
{
  "policy_vendor": "ubuntu-snappy",
  "policy_version": 1.3,
  "template": "default",
  "policy_groups": [
    "networking"
  ]
}
}}}

The defaults provided may change as the confinement needs evolve. The template policy is found in `/usr/share/apparmor/easyprof/templates/ubuntu-snappy/1.3/default` and the `networking` policy is found in `/usr/share/apparmor/easyprof/policy-groups/ubuntu-snappy/1.3/networking`.

You may provide your own security json to extend security policy in various ways by using the `apparmor` integration section. Specifying `apparmor` may trigger a manual review in the store, and is not needed for normal Snappy apps, but instead is provided for trusted snappy applications. Eg:{{{
...
services:
  - name: bar
    start: bin/bar
    ports:
        required: 80/tcp
integration:
  bar:
    apparmor: meta/bar.apparmor
}}}

With the above yaml, you can now create `meta/bar.apparmor` that contains:{{{
{
  "policy_vendor": "ubuntu-snappy",
  "policy_version": 1.3,
  "template": "default",
  "policy_groups": [
    "networking"
  ],
  "write_path": [
    "/some/path"
  ]
}
}}}
 * upon snap install, the click security json is symlinked into `/var/lib/apparmor/clicks`
 * `aa-clickhook` is run on install and places the profile in `/var/lib/apparmor/profiles` (prefixed with 'click_')
 * `aa-clickhook` is shipped in click-apparmor
 * use of `apparmor` may trigger a manual review via the store (depending on the contents of the security json)
 * see the [[https://wiki.ubuntu.com/SecurityTeam/Specifications/ApplicationConfinement/Manifest#Click|click manifest]] documentation for details

'''IMPORTANT''': due to a limitation in click, if you install an already installed snap with the same version, the click hooks may not be run. If you are trying to adjust policy, you may need to run '`sudo aa-clickhook -f`' to force regeneration of the profiles.

==== apparmor-profile hook ====
In addition to the above, specialized, hand-crafted confinement may be done via the `apparmor-profile` integration section. Specifying `apparmor-profile` will trigger a manual review in the store, and is not needed for normal Snappy apps, but instead is provided for snappy framework and trusted snappy applications.

The `aa-profile-hook` works similarly to `aa-clickhook`:
 * The snappy packaging specifies to use 'apparmor-profile' in its 'integration' section, which specifies the profile. Eg:{{{
...
services:
  - name: bar
    start: bin/bar
    ports:
        required: 80/tcp
integration:
  bar:
    apparmor-profile: meta/bar.profile
}}}
 * the profile should reference the !AppArmor variables for CLICK_DIR, APP_PKGNAME, APP_APPNAME, APP_VERSION and for profile attachment, just like the `apparmor` template does with click-apparmor
 * aa-profile-hook is run on install and places the profile in `/var/lib/apparmor/profiles` (prefixed with 'profile_')
 * If the binary is a service, `snappy-systemd` makes sure `/etc/systemd/system/<service>.service` launches the service using `ubuntu-snapp-launch` (snappy-systemd temporarily sets `AppArmorProfile=<profilename>` instead of using the launcher)
 * `aa-profile-hook` is shipped in click-apparmor
 * use of `apparmor-profile` requires manual review via the store.

To improve maintenace, the profile author can use !AppArmor variables to avoid worrying about updating the profile name, the app name, where the app is installed or knowing the package version. Example profile:{{{
#include <tunables/global>

# Specified profile variables
###VAR###

###PROFILEATTACH### (attach_disconnected) {
  #include <abstractions/base>
  #include <abstractions/nameservice>

  # Read-only for the install directory
  @{CLICK_DIR}/@{APP_PKGNAME}/ r,
  @{CLICK_DIR}/@{APP_PKGNAME}/@{APP_VERSION}/ r,
  @{CLICK_DIR}/@{APP_PKGNAME}/@{APP_VERSION}/** mrklix,

  # Writable home area
  owner @{HOMEDIRS}/apps/@{APP_PKGNAME}/ rw,
  owner @{HOMEDIRS}/apps/@{APP_PKGNAME}/** mrwklix,

  # Read-only system area for other versions
  /var/lib/apps/@{APP_PKGNAME}/ r,
  /var/lib/apps/@{APP_PKGNAME}/** mrkix,

  # Writable system area only for this version.
  /var/lib/apps/@{APP_PKGNAME}/@{APP_VERSION}/ w,
  /var/lib/apps/@{APP_PKGNAME}/@{APP_VERSION}/** wl,

  ... specialized confinement ...
}}}

Example yaml to use the profile:{{{
name: foo
version: 0.1
...
services:
  - name: bar
    start: bin/bar
    ports:
        required: 80/tcp
integration:
  bar:
    apparmor-profile: meta/bar.profile
}}}

You may not specify the `apparmor` and `apparmor-profile` for the same <appname> (eg, specifying `"apparmor-profile": "meta/appname.profile"` and `"apparmor": "meta/appname.apparmor"` is an error).

== Normal usage ==
As stated, the snappy packaging yaml does not have to do anything to specify the default confinement. Eg, the following yaml:{{{
name: foo
version: 0.1
...
services:
  - name: bar
    start: bin/bar
  - name: baz
    start: bin/baz
  - name: norf
    start: bin/norf
}}}

will create the following !AppArmor profiles with default confinement:
 * foo_bar_0.1
 * foo_baz_0.1
 * foo_norf_0.1

== Advanced usage ==
While the snappy packaging yaml is intentionally simple and straightforward for app developers, it can also quite flexible for those who need it. For example, consider the following yaml:{{{
name: foo
version: 0.1
...
services:
  - name: normal-service
    start: bin/normal-service
  - name: extra-policy-group-service
    start: bin/extra-policy-group-service
    caps: networking, extra
  - name: non-default-template-service
    start: bin/non-default-template-service
    security-template: non-default
binaries:
  - name: bin/normal-binary
  - name: extra-policy-group-binary
    caps: networking, extra
}}}

snappy will generate the following security manifests:
 * normal-service:{{{
{
  "policy_vendor": "ubuntu-snappy",
  "policy_version": 1.3,
  "template": "default",
  "policy_groups": [
    "networking"
  ]
}
}}}
 * extra-policy-group-service:{{{
{
  "policy_vendor": "ubuntu-snappy",
  "policy_version": 1.3,
  "template": "default",
  "policy_groups": [
    "networking",
    "extra"
  ]
}
}}}
 * non-default-template-service:{{{
{
  "policy_vendor": "ubuntu-snappy",
  "policy_version": 1.3,
  "template": "non-default",
  "policy_groups": [
    "networking"
  ]
}
}}}
 * normal-binary:{{{
{
  "policy_vendor": "ubuntu-snappy",
  "policy_version": 1.3,
  "template": "default",
  "policy_groups": [
    "networking"
  ]
}
}}}
 * extra-policy-group-binary:{{{
{
  "policy_vendor": "ubuntu-snappy",
  "policy_version": 1.3,
  "template": "default",
  "policy_groups": [
    "networking",
    "extra"
  ]
}
}}}

Which in turn creates the following !AppArmor profiles:
 * foo_normal-service_0.1
 * foo_extra-policy-group-service_0.1
 * foo_non-default-template-service_0.1
 * foo_normal-binary_0.1
 * foo_extra-policy-group-binary_0.1

== Debugging ==
To check to see if you have any denials:{{{
$ sudo journalctl --no-pager -k | grep DEN
}}}

A denial will look something like:{{{
An !AppArmor denial will look something like:{{{
Line 352: Line 36:
If there are no denials, !AppArmor shouldn't be blocking the app. If there are no !AppArmor denials, !AppArmor shouldn't be blocking the app.
Line 354: Line 38:
If there are denials, you can unblock yourself by:
 * modifying the profile in /var/lib/apparmor/profiles that corresponds to your app
If there are !AppArmor denials, you can unblock yourself by:
 * modifying the profile in /var/lib/snapd/apparmor/profiles that corresponds to your app
Line 357: Line 41:
$ sudo apparmor_parser -r /var/lib/apparmor/profiles/<profile> $ sudo apparmor_parser -r /var/lib/snapd/apparmor/profiles/<profile>
Line 359: Line 43:

A seccomp denial will look something like:{{{
audit: type=1326 audit(1430766107.122:16): auid=1000 uid=1000 gid=1000 ses=15 pid=1491 comm="env" exe="/bin/bash" sig=31 arch=40000028 syscall=983045 compat=0 ip=0xb6fb0bd6 code=0x0
}}}

The `syscall=983045` can be resolved with the `scmp_sys_resolver` command (you may also use the `sc-logresolve` command). Eg:{{{
$ scmp_sys_resolver 983045
set_tls
}}}

In general, if there are no seccomp denials, it shouldn't be blocking the app, however do keep in mind that if the app is somehow trying to elevate its privileges (eg, via a setuid executable) the app may receive a `Permission denied` error with no denial (see `PR_SET_NO_NEW_PRIVS` discussion above). If there are seccomp denials, you can unblock yourself by modifying the seccomp file in /var/lib/snapd/seccomp/profiles, then launch your app like normal (the launcher will pick up the change on app invocation).
Line 363: Line 58:
 * `sudo aa-clickhook -f` will regenerate all the `apparmor` profiles in /var/lib/apparmor/profiles
 * `sudo aa-profile-hook -f` will regenerate all the `apparmor-profile` profiles in /var/lib/apparmor/profiles
 * `sudo aa-status` will show you the profiles loaded in the kernel and what processes are running under them
 * `ps Z`, `ps Z <pid>` and `ps auxwwZ` will show you normal ps output, but with the apparmor label the profile is running under
 * `sudo systemctl stop <app>.service` and `sudo systemctl start <app>.service` to stop and start services from /etc/systemd/system (ie, where snappy-systemd puts the service files)
 * `sudo sysctl -w kernel.printk_ratelimit=0` will disable kernel rate limitingof denials
 * `snappy-debug.security scanlog`: follow /var/log/syslog` and show !AppArmor and seccompg denial
 * `snappy-debug.security disable-rate-limiting`: disable kernel rate limiting
 * `snappy-debug.security reload [<name>.<origin>]`: reloads apparmor security policy into the kernel
 * `snappy-debug.security regenerate [<name>.<origin>]`: regenerate apparmor security policy from snappy packaging
Line 369: Line 64:
  * In one terminal launch (tail kernel log for !AppArmor denials):{{{
$ sudo journalctl -f -k | grep DEN
  * In one terminal launch `snappy-debug.security scanlog`:{{{
$ sudo snappy-debug.security scanlog
Line 372: Line 67:
  * In another (service name is the filename in /etc/systemd/system):{{{
$ sudo journalctl -f -u <service name>
  * In another (if daemon):{{{
$ sudo journalctl -k -u <service name>| grep audit
Line 375: Line 70:
  * Then launch the app with systemctl like above, or launch manually under confinement with:{{{   * Then launch the app normally or launch manually under confinement with:{{{
Line 379: Line 74:
= Future work =
 * Confining snappy apps (particularly processes requiring root) may use !AppArmor-wrapped user namespaces.
  • Created: 2014-12-05

  • Created by: Jamie Strandboge

  • Contributors: Jamie Strandboge, Tyler Hicks

  • Packages affected: apparmor, libseccomp, ubuntu-core-security, ubuntu-snappy, ubuntu-core-launcher (historically, click-apparmor)

  • Status: Production

Introduction

Snappy confinement is an evolution of the security model for Ubuntu Touch. The basic concepts for confined applications and the AppStore model pertain to snappy applications as well. In short, applications are confined by default through the use of various technologies and this is achieved through a simple template-based system where policy is extended through the use of interfaces.

Please see the security whitepaper for the most up to date information on Ubuntu Core series 16: https://developer.ubuntu.com/en/snappy/guides/security-whitepaper/ (starting at section 'Snappy for Ubuntu Core')

Ubuntu Core 15.04 spec can be viewed here: https://wiki.ubuntu.com/SecurityTeam/Specifications/SnappyConfinement15.04

The most up to date information on snappy confinement can be found here: https://github.com/snapcore/snapd/wiki/Security

Debugging

When debugging policy issues, the snappy-debug.security tool can help. Use sudo snap install snappy-debug and then simply launch it to have it follow the logs and provide suggestions:

$ sudo snap install snappy-debug
$ sudo snap connect snappy-debug:log-observe ubuntu-core:log-observe
$ sudo /snap/bin/snappy-debug.security scanlog
...

snappy-debug.security scanlog will report both AppArmor and seccomp denials.

Alternatively you can use the lowlevel tools to check to see if you have any denials:

$ sudo journalctl --no-pager -k | grep audit

An AppArmor denial will look something like:

apparmor="DENIED" operation="mkdir" profile="foo_bar_0.1" name="/var/lib/foo" pid=637 comm="bar" requested_mask="c" denied_mask="c" fsuid=0 ouid=0

If there are no AppArmor denials, AppArmor shouldn't be blocking the app.

If there are AppArmor denials, you can unblock yourself by:

  • modifying the profile in /var/lib/snapd/apparmor/profiles that corresponds to your app
  • reload the profile with:

    $ sudo apparmor_parser -r /var/lib/snapd/apparmor/profiles/<profile>

A seccomp denial will look something like:

audit: type=1326 audit(1430766107.122:16): auid=1000 uid=1000 gid=1000 ses=15 pid=1491 comm="env" exe="/bin/bash" sig=31 arch=40000028 syscall=983045 compat=0 ip=0xb6fb0bd6 code=0x0

The syscall=983045 can be resolved with the scmp_sys_resolver command (you may also use the sc-logresolve command). Eg:

$ scmp_sys_resolver 983045
set_tls

In general, if there are no seccomp denials, it shouldn't be blocking the app, however do keep in mind that if the app is somehow trying to elevate its privileges (eg, via a setuid executable) the app may receive a Permission denied error with no denial (see PR_SET_NO_NEW_PRIVS discussion above). If there are seccomp denials, you can unblock yourself by modifying the seccomp file in /var/lib/snapd/seccomp/profiles, then launch your app like normal (the launcher will pick up the change on app invocation).

Do note that the local modification will not be preserved on package update. If you believe you have found a bug, please file a bug against: https://bugs.launchpad.net/ubuntu/+source/apparmor/+filebug

Helpful degugging commands

  • sudo sysctl -w kernel.printk_ratelimit=0 will disable kernel rate limitingof denials

  • snappy-debug.security scanlog: follow /var/log/syslog` and show AppArmor and seccompg denial

  • snappy-debug.security disable-rate-limiting: disable kernel rate limiting

  • snappy-debug.security reload [<name>.<origin>]: reloads apparmor security policy into the kernel

  • snappy-debug.security regenerate [<name>.<origin>]: regenerate apparmor security policy from snappy packaging

  • This is often helpful when developing your app or policy for it:
    • In one terminal launch snappy-debug.security scanlog:

      $ sudo snappy-debug.security scanlog
    • In another (if daemon):

      $ sudo journalctl -k -u <service name>| grep audit
    • Then launch the app normally or launch manually under confinement with:

      $ aa-exec -p <profile name> -- /apps/<pkgname>/<version>/...


CategorySpec

SecurityTeam/Specifications/SnappyConfinement (last edited 2016-11-17 16:29:21 by jdstrand)