diff --git a/README b/README
new file mode 100644
index 0000000000000000000000000000000000000000..82377d85edb09a0167ec91f058f9db8cd21d8058
--- /dev/null
+++ b/README
@@ -0,0 +1,42 @@
+Why is there 'openvpn' and 'openvpn-frontend'?
+
+The "openvpn-frontend" group is completely unrelated to float at all, its there
+to solve an ansible problem: modules/roles can't export "facts" for other roles
+to consume. So, how do you customize a role with data that belongs to something
+else?
+
+In our situation, we want to customize DNS with openvpn-specific data. The
+"openvpn" group, where the "openvpn" role runs, might not overlap with the hosts
+where dns-servers are running. So we create a new role, and tell ansible to run
+it on *top* of the *other* one. The "openvpn-frontend" thus has 'hosts:
+frontend', which is the ansible group corresponding to the role that runs dns
+servers. If ansible had a way to allow a group to indicate "we have this DNS
+information, dns-role please consume it", then this would not be necessary.
+
+The problematic bit is that we want to schedule the openvpn-frontend role on the
+'frontend' groups, and there is no openvpn-frontend service, so instead we are
+customizing the 'frontend' role
+
+If you did not define any groups in hosts.yml, float would still work, it would
+turn up nginx somewhere, backends somewhere else, and all work. But it turns
+out, when deploying to the real-world, people do care where their frontends run,
+as this is your public footprint.  So float comes with the assumption that there
+will be a "frontend" group in your invetory, and just relies on the fact that
+the "frontend" role is scheduled there with num_instances=all
+
+Why is there a '[openvpn]' group, but no host attached to it?
+
+You might have noticed that site.yml has a hosts parameter with roles assigned
+to them, and the actual hosts defined in site.yml are connected to the hosts.yml
+groups parameter. The hosts.yml has floatrp1 with the groups: [frontend], but
+there is no host which has the '[openvpn]' group attached to it.
+
+For the 'openvpn' service, there is a scheduling_group, which sets the *scope*
+of the possible hosts that the service will be scheduled onto. Float will create
+automatically a 'openvpn' group, containing just the hosts that 'openvpn' is
+running on. We did not define an 'openvpn' group in the hosts.yml ansible
+inventory, yet such a group is automatically created by float, and you can use
+it in Ansible. This 'openvpn' group is a subset of the scheduling_group.
+
+"where can I run openvpn" -> scheduling_group (frontend)
+"where is openvpn actually running" -> "openvpn" group
diff --git a/credentials/openvpn/ca.pem b/credentials/openvpn/ca.pem
new file mode 100644
index 0000000000000000000000000000000000000000..981acae529fec322e335132042c8e6ba09e07c95
--- /dev/null
+++ b/credentials/openvpn/ca.pem
@@ -0,0 +1,9 @@
+-----BEGIN CERTIFICATE-----
+MIIBRzCB76ADAgECAgEBMAoGCCqGSM49BAMCMBoxGDAWBgNVBAMTD0xFQVAgT3Bl
+blZQTiBDQTAeFw0xOTExMjYxODM3MjRaFw0yNDExMjYxODQyMjRaMBoxGDAWBgNV
+BAMTD0xFQVAgT3BlblZQTiBDQTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABDsZ
+vt/ZWnMv1k4CVrjnmPMbuP5g3449mYGhxjfTJU5ppLxYvnynlbLNX9vkCSLVP24s
+bsPVVBvan9upMM9ErTWjJjAkMA4GA1UdDwEB/wQEAwICpDASBgNVHRMBAf8ECDAG
+AQH/AgEBMAoGCCqGSM49BAMCA0cAMEQCIHps/xJhxnfoFhcbvX1Kzwtq2MVOdO1m
+J2qGeNlQZKw8AiAPVCyN1OClfOIE5kk2xehYHkt3x0V/5BMBnyjMqygk4g==
+-----END CERTIFICATE-----
diff --git a/credentials/openvpn/ca_private_key.pem b/credentials/openvpn/ca_private_key.pem
new file mode 100644
index 0000000000000000000000000000000000000000..813784d8095aadc5deaf109baca95069ab9f0e8d
--- /dev/null
+++ b/credentials/openvpn/ca_private_key.pem
@@ -0,0 +1,17 @@
+$ANSIBLE_VAULT;1.1;AES256
+36333864326664323936323837653065623132653437643362313965336235613061636136313032
+3932353263643534323038373365303065343361323566330a353164613361396461663764313863
+30343464356262323838643130343864623735346436333566383838323139313631343665363762
+3030663031396464360a373461363437336333633639623061623035613432333531646236313332
+62316662643764623465333833306439323239316563613832346231396133393563383862323831
+32306534313562643162633162383963303133396236623763396664386133633430336534336533
+66366630323565623261313835333230346634343861386561353764316135613932653261343436
+61383836396435343130386565303934376530333162666334373662646333336234366263383530
+38376639386164363236376239326566336433306339323165633734353064356363613931376239
+39626135646232373133326630323839613836626135643864333939386537333536356566633039
+66323238336661373935623636633036346339636435653839393436316238643437306632303061
+37643163373732656631333231626132333161623131343135373365383461613366643437323839
+38323537393836666661336532363735313731343461333139633730343339353635353534363936
+38353934316436303162346432623635363261346662346461633136663764303866333964366164
+35316139343436393165396438353434363234366335343432633366343438653666646661663037
+66323932303639326130
diff --git a/credentials/openvpn/cert.pem b/credentials/openvpn/cert.pem
new file mode 100644
index 0000000000000000000000000000000000000000..f35df1f48cd2c5d4c02a9fda2229ce1ecf3dd49d
--- /dev/null
+++ b/credentials/openvpn/cert.pem
@@ -0,0 +1,11 @@
+-----BEGIN CERTIFICATE-----
+MIIBjTCCATSgAwIBAgIQJvT0h0fXj84tWGMJgQt2/TAKBggqhkjOPQQDAjAaMRgw
+FgYDVQQDEw9MRUFQIE9wZW5WUE4gQ0EwHhcNMTkxMTI2MTg0NTA4WhcNMjAxMTI1
+MTg1MDA4WjASMRAwDgYDVQQDEwdvcGVudnBuMFkwEwYHKoZIzj0CAQYIKoZIzj0D
+AQcDQgAEc3k3ReiazyfDb7LW7hfuZuq2eQg1jR+aCQiUkALh9y5UP+ipwxPeKHXX
+j63r1wgQXsLFqFvqz0+07BpagxmdR6NkMGIwDgYDVR0PAQH/BAQDAgWgMBMGA1Ud
+JQQMMAoGCCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwLQYDVR0RBCYwJIIHb3BlbnZw
+boIZb3BlbnZwbi5mbG9hdC5iaXRtYXNrLm5ldDAKBggqhkjOPQQDAgNHADBEAiB1
+ND+gYgaEvVCOQDI7Kv1HohPDTZyLpC9XjjQvNSjBwQIgCjm7VXk9xTKWgJILZwmF
+Biv29JcWa1DdrrabzcsWsVY=
+-----END CERTIFICATE-----
diff --git a/credentials/openvpn/private_key.pem b/credentials/openvpn/private_key.pem
new file mode 100644
index 0000000000000000000000000000000000000000..d412350dc05eda0c27a9f8badb767e9a61770463
--- /dev/null
+++ b/credentials/openvpn/private_key.pem
@@ -0,0 +1,5 @@
+-----BEGIN EC PRIVATE KEY-----
+MHcCAQEEIEFIgj1NJg2NxYAAFDdTyr4wYHu3UIXv3ZGb7MmDA5mFoAoGCCqGSM49
+AwEHoUQDQgAEc3k3ReiazyfDb7LW7hfuZuq2eQg1jR+aCQiUkALh9y5UP+ipwxPe
+KHXXj63r1wgQXsLFqFvqz0+07BpagxmdRw==
+-----END EC PRIVATE KEY-----
diff --git a/float/.gitrepo b/float/.gitrepo
index 18d3cc786c5fa5507e9412996c599a9bda6e60c0..b8e8c7e91f79be2ded68e6f2558d711f3fcbc243 100644
--- a/float/.gitrepo
+++ b/float/.gitrepo
@@ -6,6 +6,6 @@
 [subrepo]
 	remote = https://git.autistici.org/ai3/float.git
 	branch = master
-	commit = 15ed5137e3c5b89fe12e3e795706bb0aea721f74
-	parent = 7f945908bfbe3622e842386de8686672dac44aa5
+	commit = efc7d0d3b82fbd88d781c00ce4ed11d71ca5110e
+	parent = f8e1df3360fc09f41a0b8eec30769950e05a6677
 	cmdver = 0.3.1
diff --git a/float/README.it.md b/float/README.it.md
index ca15f95d13342cb92a72f6ea37de3d1d544c0716..7da998ed04a168dd3647d837380fbcbdc93500ef 100644
--- a/float/README.it.md
+++ b/float/README.it.md
@@ -16,7 +16,7 @@ a qualcosa di più sofisticato, come Kubernetes.
 # Funzionalità
 
 Alcune di queste, specialmente se comparate con soluzioni più sofisticate,
-come Kubernetes, sono anti-caratteristiche:
+come Kubernetes, sono non-funzionalità:
 
 * *allocazione statica dei servizi* - lo scheduler del servizio non
   migra i container a runtime in caso di fallimento di un host, tutti
@@ -45,10 +45,9 @@ riusciti...
 
 # Obiettivo
 
-Dovrebbe essere chiaro dalla lista delle "Funzionalità" qui sopra:
-questo sistema non offre *high availability* senza adoperare alcune
-accortezze nei servizi stessi. La principale limitazione, rispetto a
-sistemi più evoluti, è il requisito di un'operazione manuale in caso
+Dovrebbe essere chiaro dalla lista delle "funzionalità" qui sopra:
+questo sistema non punta ad offrire *alta disponibilità*, ma ad avere qualche automazione nei servizi stessi. 
+La principale limitazione, rispetto asistemi più evoluti, è il requisito di un'operazione manuale in caso
 di cambiamenti dell'ambiente ad alto livello (fallimento di macchine,
 cambiamenti nella richiesta/offerta): per fare un esempio, se avete
 configurato un servizio con un'unica istanza su un server che poi si
@@ -70,21 +69,22 @@ Una documentazione più dettagliata è disponible nella sottocartella
 
 ### Documentazione generale
 
-* [Guida di partenza](docs/quickstart.md)
-* [Guida all'integrazione con Ansible](docs/ansible.md)
-* [Configurationi](docs/configuration.md)
-* [Protocollo Service discovery](docs/service_mesh.md)
-* [HTTP router](docs/http_router.md)
-* [Usare Docker](roles/docker/README.md)
-* [Usare gli strumenti da CLI](docs/cli.md)
-* [Sperimentazione](docs/testing.md)
+* [Guida di partenza rapida](docs/quickstart.it.md)
+* [Guida all'integrazione con Ansible](docs/ansible.it.md)
+* [Note sull'uso in produzione](docs/running.it.md)
+* [Riferimenti per le configurazioni](docs/configuration.it.md)
+* [Protocollo Service discovery](docs/service_mesh.it.md)
+* [HTTP router](docs/http_router.it.md)
+* [Usare Docker](roles/docker/README.it.md)
+* [Usare gli strumenti da CLI](docs/cli.it.md)
+* [Sperimentazione](docs/testing.it.md)
 
 ### Documentazione dei servizi integrati
 
-* [Monitoring and alerting](roles/prometheus/README.md)
-* [Gestione ed analisi dei Log](roles/log-collector/README.md)
-* [DNS pubblici e autoritativi](roles/dns/README.md)
-* [Gestione dell'identità e delle autorizzazioni](docs/identity_management.md)
+* [Monitoring and alerting](roles/prometheus/README.it.md)
+* [Gestione ed analisi dei Log](roles/log-collector/README.it.md)
+* [DNS pubblici e autoritativi](roles/dns/README.it.md)
+* [Gestione dell'identità e delle autorizzazioni](docs/identity_management.it.md)
 
 I servizi integrati sono implementati con ruoli Ansible, e non sono
 necessariamente eseguiti dentro container. Ma questo è giusto un
@@ -111,5 +111,5 @@ Nonostante non siano dei requisiti obbligatori, probabilmente vorrete
 usare alcuni servizi esterni che non sono forniti da *float* stesso:
 
 * git repository hosting
-* un sistema di CI per creare in proprio le immagini dei container
+* un sistema di CI (continuos integration) per creare in proprio le immagini dei container
 * un registro Docker
diff --git a/float/README.md b/float/README.md
index 95fb03f85496915a10e7dbc1cd9bd3e5615a72bf..5f68cb30e44657834528b61c13f08f8366770bec 100644
--- a/float/README.md
+++ b/float/README.md
@@ -42,7 +42,7 @@ those accounts.
 # Target
 
 It should be clear from the list of "features" above, but this system
-isn't meant to provide high availability without some smartness in the
+isn't meant to provide *high availability* without some smartness in the
 services themselves. Its major limitation is the requirement for
 manual operator action in face of high-level environmental changes
 (loss of machines, changes in load/demand), so for instance it won't
diff --git a/float/docs/ansible.it.md b/float/docs/ansible.it.md
new file mode 100644
index 0000000000000000000000000000000000000000..ae59f6533f4c25d26693161d20851c5593a60a89
--- /dev/null
+++ b/float/docs/ansible.it.md
@@ -0,0 +1,398 @@
+Ansible
+=======
+
+Questo documento descrive come l'infrastruttura usa Ansible, 
+come lo si usa da utente, come lo si estende come servizio in sviluppo.
+
+
+# Caratteristiche
+
+Il nostro kit di strumenti è implementato come set di pkugin e ruoli Ansible,
+significa che è integrato nelle tue proprie configurazioni Ansible. Questi
+plugin e ruoli aumentano le funzionalità di Ansible in diversi modi utili:
+
+### GPG integrata
+
+Puoi usare GPG per cifrate i file contenenti le configurazioni dei gruppi e gli host.
+Such files must have a `.yml.gpg` extension. Ansible will
+then decrypt them at runtime (use of gpg-agent is advised). Same holds
+for the *ansible-vault* password file.
+
+Useful when you're using GPG to manage the root trust repository.
+
+This functionality is implemented by the
+[gpg_vars](../plugins/vars/gpg_vars.py) plugin.
+
+### Automated credentials management
+
+Service-level credentials are automatically managed by the toolkit and
+are encrypted with *ansible-vault*. This includes an internal X509 PKI
+for TLS service authentication, a SSH PKI for hosts, and application
+credentials defined in *passwords.yml*. See the *Credentials* section
+of the [configuration reference](configuration.md) for specific
+details.
+
+All autogenerated credentials are stored in the *credentials_dir*
+specified in the top-level Float configuration, which will usually
+point at a separate git repository (or a temporary directory for test
+environments).
+
+### Integration of services and Ansible roles
+
+The toolkit defines Ansible host groups for each service, to make it
+easy to customize services with Ansible roles. For instance, suppose
+that a service *foo* is defined in *services.yml*, and you have
+created your own *foo* Ansible role to go with it (with some
+foo-specific host setup). You can then tie the two together in your
+playbook by making use of the *foo* host group:
+
+```yaml
+- hosts: foo
+  roles:
+    - foo
+```
+
+
+# Usage
+
+The toolkit lets you define container-based services, which may not
+require any configuration beyond the service definition, as well as
+Ansible-based services. It does so primarily by taking over the
+Ansible inventory (see the [configuration reference](configuration.md)
+for details). It is expected that you will have your own Ansible
+configuration, with service-specific roles and configuration,
+extending the base roles and making use of the toolkit's features.
+
+So, to use the toolkit, you will have to include it from your own
+Ansible configuration, and specifying the inventory and service
+configuration in our own format.
+
+There are some (minimal) requirements on how your Ansible environment
+should be set up for this to work:
+
+* you must have a *group_vars/all* directory (this is where we'll
+  write the autogenerated application credentials)
+* you must include *playbooks/all.yml* from the toolkit source
+  directory at the beginning of your playbook
+* you must use the *float* wrapper instead of running
+  *ansible-playbook* directly (it helps setting up the
+  command line)
+
+## Ansible environment setup how-to
+
+Let's walk through creating an example Ansible configuration for your
+project.
+
+First, check out the base *float* repository somewhere. We'll store
+everything related to this project below a top-level directory called
+*~/myproject*. We'll put the *float* repository in the *float*
+subdirectory.
+
+```shell
+$ mkdir ~/myproject
+$ git clone ... ~/myproject/float
+```
+
+Let's create the directory with our own Ansible configuration in the
+*ansible* subdirectory:
+
+```shell
+$ mkdir ~/myproject/ansible
+```
+
+And put a top-level Ansible configuration file (*ansible.cfg*) in
+there that refers to the toolkit repository location:
+
+```shell
+$ cat >ansible.cfg <<EOF
+[defaults]
+roles_path = ../float/roles:./roles
+inventory_plugins = ../float/plugins/inventory
+action_plugins = ../float/plugins/action
+vars_plugins = ../float/plugins/vars
+force_handlers = True
+
+[inventory]
+enable_plugins = float
+EOF
+```
+
+This will look for plugins and base roles in *~/myproject/float*, and
+it will load our own Ansible roles and config from
+*~/myproject/ansible*.
+
+The *force_handlers* option is important because *float* controls
+system status via handlers, and they should run even in case of
+errors.
+
+We're going to need a place to store global configuration, *float*
+requires to have a *group_vars/all* directory anyway, so we can use
+that and put some global variables in
+*~/myproject/ansible/group_vars/all/config.yml*:
+
+```shell
+$ mkdir -p ~/myproject/ansible/group_vars/all
+$ cat > ~/myproject/ansible/group_vars/all/config.yml <<EOF
+---
+domain: internal.myproject.org
+domain_public:
+  - myproject.org
+EOF
+```
+
+Then you can create the main configuration file (*float.yml*), the
+host inventory (*hosts.yml*), and the service definition
+(*services.yml*). Check out the [configuration
+reference](configuration.md) for details.
+
+Finally, we are going to set up a basic playbook in *site.yml* that
+will just run all the playbooks in the main repository:
+
+```yaml
+---
+- import_playbook: ../float/playbooks/all.yml
+```
+
+Now you can create your own service-specific Ansible configuration and
+roles based on this skeleton.
+
+## Running playbooks
+
+The *float* wrapper makes some necessary fixes to the
+environment and invokes *ansible-playbook* with the same command-line
+arguments, so you should use its *run* command whenever you would use
+*ansible-playbook*.
+
+The ansible-vault setup is mandatory, so you are going to have to pass
+the location of the ansible-vault encryption passphrase to Ansible via
+the environment. Just use the `ANSIBLE_VAULT_PASSWORD_FILE` variable
+as you normally would, with one additional feature: if the filename
+ends in *.gpg*, the passphrase will be decrypted using GPG.
+
+With respect to the previous example:
+
+```shell
+$ echo secret > vault-pw
+$ ANSIBLE_VAULT_PASSWORD_FILE=vault-pw \
+    ../float/float run --config=config.yml site.yml
+```
+
+## Initialize the permanent credentials
+
+Before you can run Ansible to set up the services in your config,
+there is one more step that needs to be done.  In order to bootstrap
+the internal PKIs, and generate the application credentials (which are
+also valid forever, or until revoked of course), you need to invoke
+the playbook in *playbooks/init-credentials.yml*:
+
+```shell
+$ ANSIBLE_VAULT_PASSWORD_FILE=vault-pw \
+    ../float/float run --config=config.yml init-credentials
+```
+
+This will write a bunch of files in your *credentials_dir*, including
+the private keys for the various PKIs (X509, SSH, etc), and a
+*secrets.yml* file containing the autogenerated application
+credentials.
+
+These files are of course to be kept private when setting up a
+production environment.
+
+
+## Credentials
+
+The system uses two major types of credentials:
+
+* *managed* credentials for accounts, services, etc - these can be
+  autogenerated automatically, based on the top-level description in
+  *passwords.yml*. They are stored encrypted with *ansible-vault*.
+* *root* credentials, that need to be provided externally, including
+  for instance the *ansible-vault* password used for managed
+  credentials, and other third-party secrets (like credentials for a
+  private Docker registry, etc).
+
+All services that require per-host secrets, such as SSH and the
+internal X509 PKI, manage those secrets directly on the hosts
+themselves, renewing them when necessary. Those secrets are not stored
+in a central location.
+
+This means that for normal usage (i.e. except when new credentials are
+added), the credentials repository is read-only, which makes it easier
+to integrate deployment with CI systems.
+
+The expectation is that, for production environments, it will be saved
+in private git repositories. Temporary setups such as test
+environments, on the other hand, need no persistence as managed
+credentials can simply be re-created every time.
+
+
+# Implementation details
+
+These are details of how parts of the infrastructure is implemented,
+useful if you want to understand how a service is deployed, or how to
+write a new one.
+
+## Scheduler
+
+The *float* service scheduler is implemented as an Ansible dynamic
+inventory plugin. All it does is set a number of global and host
+variables for the float Ansible roles to use. It's useful to know what
+these are, in case you want to write your own Ansible roles.
+
+### Variables
+
+The *float* service scheduler sets a large number of host variables
+and global configuration parameters. 
+
+The following global variables are defined:
+
+* `services` holds all the service metadata, in a dictionary indexed
+  by service name;
+
+Other variables are defined in *hostvars* and are different on every
+host depending on the service assignments:
+
+* `float_enable_<service>` for each service that evaluates to
+  true on the hosts assigned to the service (note: dashes in the
+  service name are converted to underscores)
+* `float_instance_index_<service>` is the progressive index of the
+  current instance of the service (0-based).
+* `float_enabled_services` contains the list of enabled services on this
+  host
+* `float_disabled_services` contains the list of disabled services on
+  this host
+* `float_enabled_containers` contains a list of dictionaries describing the
+  containers that should be active on this host. The dictionaries have
+  the following attributes:
+  * `service` is the service metadata
+  * `container` is the container metadata
+* `float_<service>_is_master` is true on the host where the master
+  instance is scheduled, and false elsewhere. This variable is only
+  defined for services using static master election (i.e. where
+  *master_election* is true in the service metadata)
+
+### Groups
+
+The scheduler also defines new dynamic Ansible groups based on
+service assignments:
+
+* For each service, create a host group named after the service, whose
+  members are the hosts assigned to the service.
+* For each network overlay defined in the inventory, create a host
+  group named `overlay-<name>` whose members are the hosts on that
+  overlay.
+
+Float depends on the *frontend* group being defined by the user.
+
+# Extending the built-in services
+
+Most configurable aspects of the built-in services (monitoring, HTTP
+routing, etc) are parameterized in *services.yml*. But there are
+limits to what makes sense to put there, to avoid unbounded complexity
+in the service definition spec. Examples could be:
+
+* new monitoring configuration, such as probers or other custom config
+* custom HTTP configuration
+* custom DNS zones
+
+In those cases, the simplest solution is to create a separate Ansible
+role to be run on the *frontend* nodes and install whatever
+configuration files you need. Unfortunately, due to Ansible not having
+"global" handlers, you are then going to have to provide your own
+service reload handlers.
+
+The following chapter shows an example in detail: adding a custom DNS
+zone to the built-in DNS service.
+
+## Custom DNS zone
+
+If you want to set up a custom DNS zone, one way to do so is with a
+dedicated Ansible role (to be run on hosts in the *frontend* group)
+that installs your desired zonetool configuration.
+
+Let's walk through a complete example: suppose we have a service
+*myservice* that should serve HTTP requests for the *myservice.org*
+domain. This doesn't match the *service_name*.*domain* scheme that is
+expected for services described in *services.yml*, so float won't
+automatically generate its DNS configuration.
+
+What we need to do is set up the *myservice.org* DNS zone ourselves,
+and then tell float to associate that domain to the *myservice*
+service.
+
+First, we create a new Ansible role that we are going to call
+*myservice-dns*, so in the root of your Ansible config:
+
+```shell
+$ mkdir -p roles/myservice-dns/{handlers,tasks,templates}
+```
+
+The only task in the role should install a zonetool DNS configuration
+file into */etc/dns/manual*, so in
+*roles/myservice-dns/tasks/main.yml* we'll have:
+
+```yaml
+---
+
+- name: Install myservice DNS configuration
+  template:
+    src: myservice.yml.j2
+    dest: /etc/dns/manual/myservice.yml
+  notify: reload DNS
+```
+
+The DNS configuration in our case is very simple and just points "www"
+and the top-level domain at the frontends. We do so by extending the
+*@base* zone template defined by float. The contents of
+*roles/myservice-dns/templates/myservice.yml.j2* should be:
+
+```yaml
+---
+
+myservice.org:
+  EXTENDS: "@base"
+  www: CNAME www.{{ domain_public[0] }}.
+```
+
+This points the www domain at the frontends via a CNAME (all the
+*domain_public* DNS zones are already autogenerated by float). We
+could have just as easily used A records but this is simpler and works
+with both IPv4 and IPv6.
+
+Finally, we need a handler to reload the updated DNS configuration,
+which goes in *roles/myservice-dns/handlers/main.yml* and runs a shell
+command to update zonetool:
+
+```yaml
+---
+
+- listen: reload DNS
+  shell: "/usr/sbin/update-dns && rndc reload"
+```
+
+With the above we have a complete Ansible role that configures DNS for
+the *myservice.org* domain. We need to tell Ansible that this role
+needs to run on the hosts in the *frontend* group, so in your playbook
+you should have:
+
+```yaml
+- hosts: frontend
+  roles:
+    - myservice-dns
+```
+
+And to complete our configuration, the service description for
+*myservice* should have a *public_endpoint* directive including the
+domain, so that the float HTTP router knows where to send the
+requests:
+
+```yaml
+myservice:
+  ...
+  public_endpoints:
+    - name: myservice
+      domains:
+        - www.myservice.org
+        - myservice.org
+      port: ...
+```
diff --git a/float/docs/quickstart.it.md b/float/docs/quickstart.it.md
new file mode 100644
index 0000000000000000000000000000000000000000..b852583f8edc17551d028932f5717ddbe1a20461
--- /dev/null
+++ b/float/docs/quickstart.it.md
@@ -0,0 +1,209 @@
+Guida di partenza rapida
+===
+
+In questo documento useremo *float* con delle semplici configurazioni,
+e per usarlo con un servizio HTTP basico su una macchina virtuale, usando
+Vagrant e Virtualbox.
+
+Useremo come servizio di esempio 
+[docker/okserver](https://git.autistici.org/ai3/docker/okserver), un server HTTP davvero semplice, che risponde "OK" a tutte le richieste.
+
+## Passo 1: Dipendenze di installazione necessarie
+
+Avrai bisogno di una versione di ansible aggiornata
+[Ansible](https://ansible.com) (>= 2.7), gli strumenti di gestione delle virtual machine (Vagrant and Virtualbox), ed altri piccoli strumenti personalizzati per gestire le credenziali, che ci andremo a costruire.
+
+Inoltre, se il tuo sistema operativo usa una differente versione di Python rispetto a quella che usa Ansible, come nel caso di Debian Buster (Python
+2.7 è di default ma Ansible usa Python 3), ti toccherà installare alcuni pacchetti Python che dovrebbero essere normalmente già installati cone Ansible, come Jinja2 e PyYAML.
+
+L'ultima versione di Debian stable al momento (*bus	git@git.autistici.org:ai3/float.gitter*) non pacchettizza più Virtualbox, quindi dovrai [scaricarlo e installarlo a mano](https://www.virtualbox.org/wiki/Linux_Downloads). Il resto delle dipendenze possono essere installate con questo comando:
+
+```shell
+sudo apt install golang bind9utils ansible vagrant python-jinja2 python-yaml python-six
+go get -u git.autistici.org/ale/x509ca
+go get -u git.autistici.org/ale/ed25519gen
+export PATH=$PATH:$HOME/go/bin
+```
+
+*Float* dovrebbe lavorare ugualmente bene sia con Python 2 che con Python 3, e supportare lo scenario in cui l'interprete Python usato da Ansible è differente da quello che il sistema operativo usa di default.
+
+### Alternativa: libvirt
+
+Se davvero non ti piace Virtualbox e non vuoi installarlo manualmente, c'è l'opzione di utilizzare al suo posto *libvirt*. In Debian,
+installa i seguento pacchetti per configurare un ambiente locale per libvirt:
+
+```shell
+sudo apt install libvirt-clients libvirt-daemon-system vagrant-libvirt
+```
+
+Where specific steps need to be performed for libvirt versus
+virtualbox, this will be called out in the text with a *\[libvirt\]*
+tag.
+
+## Passo 2: Configurare un nuovo ambiente
+
+Un *ambiente* è soltanto un nome per una specifica configurazione di hosts e servizi: per convenienza, poichè è fatto da un consistente numero di file di configurazioni, lo andremo a mettere in una cartella che porta il suo nome.
+
+Assumiamo che hai scaricato il codice di *float* in `$HOME/float`,
+e che vogliamo creare le configurazioni per fare andare il nostro ambiente di test nella cartella `$HOME/float-test`. Possiamo creare le configurazioni del nostro ambiente di test usando il comando `float` dal teminale (CLI):
+
+```shell
+$HOME/float/float create-env \
+    --domain=example.com --net=192.168.10.0 \
+    --vagrant --num-hosts=1 \
+    $HOME/float-test
+```
+
+> \[libvirt\] Aggiunti nella riga di comando l'opzione 
+> *--libvirt=localhost* 
+> nell'invocazione del comando sopra "float create-env".
+
+Il comando *create-env* creerà un gruppo di file di configurazioni nella cartella *float-test*. Qui noi gli abbiam detto di usare *example.com* come dominio di base per i nostri servizi pubblici e interni, e per generare una configurazione host basata su Vagrant di una singola VM,
+usando il network privato 192.168.10.0/24 (usato dalle VMs per parlare una con l'altra). Al *create-env* l'automazione di Vagrant assegnerà gli IPs a quel network di VMs, iniziando dal numero 10, quindi la tua VM di test avrà l'indirizzo 192.168.10.10.
+
+La cartella *float-test* dovrebbe ora contenere vari file di configurazioni per Ansible e Vagrant, *create-env* li ha riempiti con i valori di default. Andiamo a dare una occhiata più da vicino che cosa sono:
+
+* `ansible.cfg` è il file di configurazioni di Ansible, che dice ad Ansible
+ dove trovare il plugin float
+* `Vagrantfile` è il file di configurazione di Vagrant che descrive la nostra singola VM (SO, ip, memory,...).
+* `config.yml` è il principale file di configurazione di *float* che principalmente solo punta al luogo delle altre configurazioni. Non c'è niente da cambiare qui, *create-env* ha già scritto dei default ragionevoli.
+* `hosts.yml` è il file inventario di Ansible(in formato YAML come richiesto da *float*), che già contiene le nostre VM di test.
+* `passwords.yml` descrive le credenziali dell'applicazione per i ruoli di Ansible, ma non lo stiamo usando quindi puoi lasciarlo intonso.
+* `services.yml` contiene la descrizione dei servizi che vogliamo far andare (nessuno per il momento).
+* `site.yml` è il nostro top-level dell'Ansible playbook.
+* `group_vars/all/config.yml` contine le configurazini globali di Ansible,
+  incluse le credentiali per gli utenti amministrativi (operators):
+  *create-env* automaticamente genera di default un utente *admin*, con
+  password *password*.
+
+Si può leggere [Riferimenti per le configurazioni](configuration.it.md) per la sintassi dei file di configurazioni e cosa significa le varie opzioni.
+
+Questa cartella è anche la tua cartella top-level di Ansible top-level, così è possibile aggiungere host_vars, group_vars, etc. come vuoi. Noi non andiamo a vedere e non ci servono quelle funzionalità per questo esempio.
+
+## Passo 3: Personalizzare l'ambiente
+
+Vogliamo dire a *float* di far andare una istanza del nostro semplice servizio HTTP dietro al suo HTTP router pubblico, ed averlo disponibile a
+*ok.example.com*. Il servizio è disponibile come immagine Docker con il nome *registry.git.autistici.org/ai3/docker/okserver*.
+
+Andiamo ad aggiungere il servizio specifico al file *services.yml* che è stato automaticamente creato dentro alla cartella *float-test*. Dato che tutti i servizi in float hanno le porte assegnate staticamente, andiamo a prendere la porta 3100 (che sappiamo essere libera). Il file *services.yml* già contiene una sezione "include" che include la definizione per tutti i servizi integrati, quindi abbiamo bisogno di aggiungere solo un pezzettino alla fine del file YAML:
+
+```yaml
+ok:
+  scheduling_group: all
+  num_instances: 1
+  containers:
+    - name: http
+      image: registry.git.autistici.org/ai3/docker/okserver:master
+      port: 3100
+      env:
+        PORT: 3100
+  public_endpoints:
+    - name: ok
+      port: 3100
+      scheme: http
+```
+
+Queste sono tutte le configurazioni che abbiam bisogno per impostare il servizio ed esportarlo tramite il router HTTP pubblico.
+
+## Passo 4: Inizializzare le credenziali
+
+Ora che le configurazioni sono pronte, abbiamo bisogno di inizializzare le credenziali a lungo termine come il PKI e la chiave di root dell'SSO, e le password dell'applicazione. Questo è uno step separato (che usa un playbook Ansible dedicato), come credenziali a lungo temine possono essere generate una volta e poi conservate per sempre. Questa separazione non è molto importante ora dato che stiamo lavorando in un ambiente di test, ma è utile per unificare i rispettivi workflow di test e di produzione. Per la stessa ragione, anche se non è strettamente necessario, stiamo andando ad usare Ansible Vault per cifrare le credenziali autogenerate.
+
+Primo, andiamo a mettre la passphrase di Ansible Vault e salviamola in un file. Puoi utilizzare anche GPG per cifrare questo file (ricordandoti di dargli come estensione `.gpg`), ma non stiamo facendolo così adesso:
+
+```shell
+cd $HOME/float-test
+echo -n passphrase > .ansible_vault_pw
+export ANSIBLE_VAULT_PASSWORD_FILE=$PWD/.ansible_vault_pw
+```
+
+Puoi scegliere una qualunque passphrase, certo. La variabile di ambiente
+*ANSIBLE_VAULT_PASSWORD_FILE* è ora impostata come abbiamo detto ad
+Ansible che passphrase deve usare, e necessiteremo di impostarla tutte le volte che vogliamo invocare Ansible tramite *float*.
+
+Possiamo inizializzare le credenziali, che di default saranno archiviate dentro la cartella *float-test/credentials/* (il valore indicato in
+*credentials_dir* in *config.yml*):
+
+```shell
+cd $HOME/float-test
+$HOME/float/float init-credentials --config=config.yml
+```
+
+Che avrà come risultato la creazione di un numero di file dentro a
+*float-test/credentials/*, con i segreti cifrati con la tua passphrase di Ansible Vault.
+
+## Passo 5: Far partire Ansible
+
+Ora siam pronti a tirare su la VM di test e fa andare Ansible su di essa per configurare i nostri servizi:
+
+```shell
+cd $HOME/float-test
+vagrant up
+$HOME/float/float run --config=config.yml site.yml
+```
+
+> \[libvirt\] dovresti mettere l'opzione *--provider=libvirt* al comando
+> "vagrant up".
+
+Quando Ansible termina con successo (e ci metterò qualche minuto la prima volta, per scaricare i pacchetti e l'immagine Docker), la macchina virtuale di test è propriamente configurata per servirci il nostro servizio
+*ok.example.com*!
+
+## Passo 6: Verificare che funzioni
+
+Il servizio ok.example.com dovrebbe essere servito dal router HTTP pubblico al nostro IP pubblico del nostro host di test, che Vagrant ha impostato come default che è
+192.168.10.10:
+
+```shell
+curl -k --resolve ok.example.com:443:192.168.10.10 https://ok.example.com
+```
+
+Se il comando risponde "OK", il servizio funziona come dovere.
+Però, se ci sono problemi, potresti voler debuggare qualche cosa che è andato storto! Ci sono un buon numero di strumenti che potresti usare per farlo:
+
+Nell'ambiente di test, abbiamo impostato un SOCKS5 proxy sulla porta 9051 nel primo host del gruppo di *frontend* (quindi in questo caso, comunque
+192.168.10.10). Questo è molto utile per simulare se la risoluzione dei DNS è appropriata e per navigare i servizi integrati senza complessi cambi all'ambiente dei tuoi host, puoi per esempio far partire un browser con:
+
+```shell
+chromium --proxy-server=socks5://192.168.10.10:9051
+```
+
+Una alternativa può essere aggiungere tutti i servizi integrati al tuo file */etc/hosts*, puntando a 192.168.10.10.
+
+Utili servizi integrati per debugging:
+
+* https://logs.example.com/ punta alla Kibana UI per il servizio di collettore di log centralizzato
+* https://grafana.example.com/ è un pannello di monitoring
+* https://monitor.example.com/ è l'interfaccia grafica minimale del sistema di monitoring di Prometheus
+
+Ovviamente puoi anche loggarti nella macchina virtuale stessa (*vagrant ssh
+host1*) ed esaminare lo stato delle cose da là. Nell'ambiente di test, 
+syslog logs sono copiati nel file */var/log/remote/*, che a volte potrebbe essere più semplice da vedere che la UI di Kibana.
+
+Una volta completati i test, non dimenticarsi di fermare le virtual
+machines lanciando (sempre dalla directory dell'ambiente di test):
+
+```shell
+vagrant destroy -f
+```
+
+Per controllare l'esecuzione delle VM, ad esempio per sospendere le VM in
+mancanza di memoria i comandi sono questi:
+
+```shell
+vagrant suspend
+vagrant resume
+```
+
+# Prossimi passi
+
+Vai a leggere  [Note sull'uso in produzione](running.it.md),
+e i [Riferimenti per le configurazioni](docs/configuration.it.md)!
+
+# Appendice: ma è così lento!
+
+Si, Ansible è generalmente abbastanza lento a fare le cose, per un numero di ragioni (tra queste i lfatto che creiamo un gran numero di task magari dato dalla possibilità che non usiamo in modo ottimale i loop). Ma ci sono un po di cose che è possibile fare che aiutono un poco:
+
+1. Installa [Mitogen](https://mitogen.networkgenomics.com/ansible_detailed.html), che nel nostro caso rende Ansible circa 5-10 più veloce. Per attivarlo, bisogna solo modificare le tue configurazioni nel file *ansible.cfg* come è mostrato nella documentazione di Mitogen docs, o semplicemente fa *--mitogen=PATH* nella linea di comandocome opzione dopo l'invocazione di  *float create-env*.
+2. Imposta una APT cache (per esempio con *apt-cacher-ng*). Imposta la variabile Ansible *apt_proxy* con host:port della cache. Quando usi Vagrant come nell'esempio fatto sopra, tieni in considerazione che il tuo host è sempre raggiungibile dalla VMs come IP .1 IP nel network privato (quindi dovrebbe essere 192.168.10.1 nell'esempio).
+
+Ancora, la prima volta che lanci le cose, tutto sarà effettuato tramite trasferimenti via rete e dalle installazioni dei pacchetti (sfortunatamente le immagini Docker sono piuttosto grosse).
diff --git a/float/docs/quickstart.md b/float/docs/quickstart.md
index e5f64dd70047fd1b36dd7946b06808eb20c682fd..893756a9dfa1a399066039ccb312965babdc1dc1 100644
--- a/float/docs/quickstart.md
+++ b/float/docs/quickstart.md
@@ -91,7 +91,7 @@ files for Ansible and Vagrant, with default values filled in by
 * `ansible.cfg` is the Ansible configuration file, which tells Ansible
   where to find the float plugins
 * `Vagrantfile` is the Vagrant configuration file describing our
-  single VM.
+  single VM (SO, ip, memory,...).
 * `config.yml` is the main *float* configuration file, which mostly
   just points at the location of the other configs. There is nothing
   to change here, as *create-env* already wrote sensible defaults.
@@ -250,6 +250,22 @@ host1*) and examine the state of things there. On testing
 environments, syslog logs are also dumped to files below
 */var/log/remote/*, which might be simpler than using the Kibana UI.
 
+To manage the VM running, for example to suspend the VM for lack of memory,
+you can use this commands:
+
+```shell
+vagrant suspend
+vagrant resume
+```
+
+When you finish the testing, not forget to destroy the virtual machine, running this command 
+in the CLI (always inside of the directory of test enviroment):
+
+```shell
+vagrant destroy -f
+```
+
+
 # Next steps
 
 Read on to [running float in a production environment](running.md),
diff --git a/float/float b/float/float
index 91c3d7e81727d54f1403023959e10c7465c596fc..d87604a3ca82a5b3f92c2eb16b9724c09ae418ff 100755
--- a/float/float
+++ b/float/float
@@ -75,7 +75,7 @@ group_vars:
     ansible_become: true
     ansible_ssh_private_key_file: "~/.vagrant.d/insecure_private_key"
 {% if libvirt and libvirt != 'localhost' %}
-    ansible_ssh_extra_args = "-o ProxyJump={{ libvirt }}"
+    ansible_ssh_extra_args: "-o ProxyJump={{ libvirt }}"
 {% endif %}
 {% else %}
 # REPLACE THESE WITH YOUR OWN HOSTS!
@@ -267,6 +267,9 @@ def command_create_env(path, domain, vagrant, mitogen, libvirt, num_hosts, net,
             'shard_id': ('host%d' % (i+1)) if i > 0 else None,
             'groups': ['frontend', 'vagrant'] if i == 0 else ['backend', 'vagrant'],
         } for i in range(num_hosts)]
+        # Special case if there's just one host: add the backend group to it!
+        if len(hosts) == 1:
+            hosts[0]['groups'].append('backend')
         vars['vagrant_box'] = 'debian/%s64' % debian_dist
     else:
         hosts = [{
diff --git a/float/playbooks/init-credentials.yml b/float/playbooks/init-credentials.yml
index 817f8ad193081731f671cc4f2d6913bfdacc8aed..555b0ba4c0967a9e6645ab0732821dc7a9ac075c 100644
--- a/float/playbooks/init-credentials.yml
+++ b/float/playbooks/init-credentials.yml
@@ -14,6 +14,10 @@
   gather_facts: no
   become: no
   tasks:
+    # Check that "float" is properly set up.
+    - include_role:
+        name: float-plugin-check
+
     # Create the paths below credentials_dir that we're going to use.
     - name: "Create paths below {{ credentials_dir }}"
       file:
diff --git a/float/plugins/action/x509_csr.py b/float/plugins/action/x509_csr.py
index c293117f0ec72097b69a86ad7c4f114eca3cda2e..ab9c3614fd21840820dea6d33620a9351269ce95 100644
--- a/float/plugins/action/x509_csr.py
+++ b/float/plugins/action/x509_csr.py
@@ -15,9 +15,6 @@ class ActionModule(ActionBase):
     TRANSFERS_FILES = False
 
     def run(self, tmp=None, task_vars=None):
-        # Recover important data from Ansible facts.
-        hostname = self._templar.template('{{inventory_hostname}}')
-
         # Retrieve action parameters.
         credentials_name = self._task.args['credentials_name']
         domain = self._task.args['domain']
@@ -38,10 +35,10 @@ class ActionModule(ActionBase):
         elif mode == 'server':
             is_server = True
             # For each name, use the short version and the FQDN.
-            for n in params['names']:
+            for n in params.get('names', []):
                 names.append('%s.%s' % (n, domain))
                 names.append(n)
-            ip_addrs = params['addrs']
+            ip_addrs = params.get('addrs', [])
         else:
           raise Exception('mode must be client or server')
 
diff --git a/float/plugins/inventory/float.py b/float/plugins/inventory/float.py
index 05b072bd4b63a3856f2846ab4b12f9e98531594f..01884794c865fe61b1c1add70c548db461b1e8f1 100644
--- a/float/plugins/inventory/float.py
+++ b/float/plugins/inventory/float.py
@@ -339,7 +339,7 @@ def _build_horizontal_upstreams_map(services):
         for ep in svc.get('horizontal_endpoints', []):
             upstream_name = 'be_%s_%s_%s' % (name, ep['port'], service_name)
             upstreams[upstream_name] = {
-                'namme': upstream_name,
+                'name': upstream_name,
                 'service_name': service_name,
                 'port': ep['port'],
                 'enable_sso_proxy': False,
@@ -530,6 +530,7 @@ def run_scheduler(config):
     all_vars = inventory.setdefault('group_vars', {}).setdefault('all', {})
     all_vars.update(config['vars'])
     all_vars.update({
+        'float_plugin_loaded': True,
         'services': services,
         'default_service_credentials': DEFAULT_SERVICE_CREDENTIALS,
         'float_global_dns_map': _global_dns_map(inventory),
diff --git a/float/roles/base/meta/main.yml b/float/roles/base/meta/main.yml
new file mode 100644
index 0000000000000000000000000000000000000000..c62444d5d89ea0a13c61729463005cc7c3ba3c2e
--- /dev/null
+++ b/float/roles/base/meta/main.yml
@@ -0,0 +1,3 @@
+---
+dependencies:
+  - { role: float-plugin-check }
diff --git a/float/roles/base/tasks/backup.yml b/float/roles/base/tasks/backup.yml
index ea901e7e7245823e68e92e4fb6d79c2a71564124..483cad483148da2f1513f028a1634c62d3d94d89 100644
--- a/float/roles/base/tasks/backup.yml
+++ b/float/roles/base/tasks/backup.yml
@@ -20,6 +20,7 @@
     - /etc/tabacco/sources
     - /etc/tabacco/handlers
     - /var/lib/tabacco
+    - /var/cache/tabacco
 
 - name: Create backup agent config
   template:
@@ -29,13 +30,6 @@
   notify:
     - reload backup agent
 
-- name: Install backup agent systemd unit
-  template:
-    src: tabacco/agent.service.j2
-    dest: /etc/systemd/system/tabacco-agent.service
-  notify:
-    - restart backup agent
-
 - name: Enable backup agent systemd unit
   systemd:
     name: tabacco-agent.service
diff --git a/float/roles/base/tasks/prometheus.yml b/float/roles/base/tasks/prometheus.yml
index 254c7bbc145cd481f7877b593b128fc69d9a9ba4..dc22d3dadb722315f4fd0b6576f53b87c54d6f56 100644
--- a/float/roles/base/tasks/prometheus.yml
+++ b/float/roles/base/tasks/prometheus.yml
@@ -38,7 +38,7 @@
 - name: Install node-exporter-cron cron job
   copy:
     dest: "/etc/cron.d/node-exporter-scripts"
-    content: "32 * * * * root /usr/local/bin/node-exporter-cron\n"
+    content: "2-59/5 * * * * root /usr/local/bin/node-exporter-cron\n"
 
 - name: Create node-exporter scripts directory
   file:
diff --git a/float/roles/base/templates/tabacco/agent.service.j2 b/float/roles/base/templates/tabacco/agent.service.j2
deleted file mode 100644
index 789eae508d440a5e2bad5f4e0c8f47a85f4ce870..0000000000000000000000000000000000000000
--- a/float/roles/base/templates/tabacco/agent.service.j2
+++ /dev/null
@@ -1,15 +0,0 @@
-[Unit]
-Description=Backup Agent
-After=network.target
-
-[Service]
-Type=simple
-ExecStart=/usr/bin/tabacco agent
-ExecReload=/bin/kill -HUP $MAINPID
-Restart=on-failure
-User=root
-Group=tabacco
-LimitNOFILE=65535
-
-[Install]
-WantedBy=multi-user.target
diff --git a/float/roles/base/templates/tabacco/agent.yml.j2 b/float/roles/base/templates/tabacco/agent.yml.j2
index 31c74913b129f55a13c4a43b22e0b5ef5aa44d90..7350945ec516831fe7aa00eae27dfe9a7ec81308 100644
--- a/float/roles/base/templates/tabacco/agent.yml.j2
+++ b/float/roles/base/templates/tabacco/agent.yml.j2
@@ -17,3 +17,4 @@ repository:
   params:
     uri: "{{ backup_repository_uri }}"
     password: "{{ backup_repository_restic_password }}"
+    cache_dir: /var/cache/tabacco
diff --git a/float/roles/float-plugin-check/tasks/main.yml b/float/roles/float-plugin-check/tasks/main.yml
new file mode 100644
index 0000000000000000000000000000000000000000..166ab4baa2dbf43c3401db48ba110f36f43786cf
--- /dev/null
+++ b/float/roles/float-plugin-check/tasks/main.yml
@@ -0,0 +1,6 @@
+---
+
+- fail:
+    msg: "The 'float' plugin was not loaded! This could be due to multiple reasons: wrong paths in ansible.cfg, problems with the Python version used by Ansible, etc..."
+  when: "not float_plugin_loaded | default(False)"
+
diff --git a/float/roles/prometheus/files/rules/alerts_backup.conf.yml b/float/roles/prometheus/files/rules/alerts_backup.conf.yml
new file mode 100644
index 0000000000000000000000000000000000000000..2ead8172b777f3057c054078d0955ab171ed58a4
--- /dev/null
+++ b/float/roles/prometheus/files/rules/alerts_backup.conf.yml
@@ -0,0 +1,11 @@
+groups:
+- name: roles/prometheus/files/rules/alerts_backup.conf
+  rules:
+  - alert: BackupFailed
+    expr: backup_ok != 1
+    for: 15m
+    labels:
+      severity: warn
+    annotations:
+      summary: '{{ $labels.dataset }} backup failure on {{ $labels.host }}'
+      description: 'Dataset {{ $labels.dataset }} has failed its backups on {{ $labels.host }}.'
diff --git a/playbooks/init-credentials.yml b/playbooks/init-credentials.yml
new file mode 100644
index 0000000000000000000000000000000000000000..e4c16b2ff31c8d57f475c206f2447e62cd2d78be
--- /dev/null
+++ b/playbooks/init-credentials.yml
@@ -0,0 +1,55 @@
+- hosts: localhost
+  gather_facts: no
+  become: no
+  tasks:
+    # Create the openvpn path below credentials_dir that we're going to use.
+    - name: "Create paths below {{ credentials_dir }}/openvpn"
+      file:
+        path: "{{ credentials_dir }}/openvpn"
+        state: directory
+
+    - name: Generate the X509 CA certificate
+      local_action: x509_ca ca_subject="{{ x509_ca_subject | default('CN=LEAP OpenVPN CA') }}" ca_cert_path="{{ credentials_dir }}/openvpn/ca.pem" ca_key_path="{{ credentials_dir }}/openvpn/ca_private_key.pem"
+
+    - name: Check the certificate for OpenVPN
+      x509_csr:
+        credentials_name: openvpn
+        domain: "{{ domain_public[0] }}"
+        mode: server
+        params:  { names: [ 'openvpn' ] }
+        private_key_path: "{{ credentials_dir}}/openvpn/private_key.pem"
+        cert_path: "{{ credentials_dir}}/openvpn/cert.pem"
+        ca_cert_path: "{{ credentials_dir }}/openvpn/ca.pem"
+        check: true
+      check_mode: no
+      register: x509_should_update
+
+    - name: Create the CSR for OpenVPN cert
+      x509_csr:
+        credentials_name: openvpn
+        domain: "{{ domain_public[0] }}"
+        mode: server
+        params: { names: [ 'openvpn' ] }
+        private_key_path: "{{ credentials_dir }}/openvpn/private_key.pem"
+        check: false
+      when: "x509_should_update.changed"
+      register: x509_csr
+
+    - name: Create the certificate for OpenVPN
+      x509_sign:
+        csr: "{{ x509_csr.csr }}"
+        mode: server
+        ca_cert_path: "{{ credentials_dir }}/openvpn/ca.pem"
+        ca_key_path: "{{ credentials_dir }}/openvpn/ca_private_key.pem"
+      when: "x509_should_update.changed"
+      register: x509_sign
+
+    - name: Install the signed certificate for OpenVPN
+      copy:
+        dest: "{{ credentials_dir}}/openvpn/cert.pem"
+        content: "{{ x509_sign.cert }}"
+        mode: 0644
+      when: "x509_sign.changed"
+
+- name: Include float init-credentials
+  import_playbook: ../float/playbooks/init-credentials.yml
diff --git a/roles/README b/roles/README
new file mode 100644
index 0000000000000000000000000000000000000000..735aae8710c6dee03e6159d01ebacaa6d9368e0a
--- /dev/null
+++ b/roles/README
@@ -0,0 +1,10 @@
+Roles:
+
+openvpn: this role conains the firewall modifications for openvpn (the firewall
+should go with the services, because the port, eg. 1194, "belongs" to the
+openvpn service, regardless of where it is scheduled), along with the general
+openvpn configuration information
+
+
+openvpn-frontend: this role will be scheduled on http/dns servers, so this role
+contains the dns modifications
diff --git a/roles/openvpn-frontend/files/openvpn_dns.yml b/roles/openvpn-frontend/files/openvpn_dns.yml
new file mode 100644
index 0000000000000000000000000000000000000000..02458407dfa0b11e2e66a10a8ea1941c847b0bec
--- /dev/null
+++ b/roles/openvpn-frontend/files/openvpn_dns.yml
@@ -0,0 +1 @@
+{{ domain }}: openvpn: "{{ groups['openvpn'] | map(attribute='ip') | tojson }}"
diff --git a/roles/openvpn-frontend/handlers/main.yml b/roles/openvpn-frontend/handlers/main.yml
new file mode 100644
index 0000000000000000000000000000000000000000..7a8122f91bbf8e6362391fa6c13091d26a896f74
--- /dev/null
+++ b/roles/openvpn-frontend/handlers/main.yml
@@ -0,0 +1,3 @@
+
+- listen: reload openvpn DNS config
+  shell: "/usr/sbin/update-dns && rndc reload"
diff --git a/roles/openvpn-frontend/tasks/main.yml b/roles/openvpn-frontend/tasks/main.yml
new file mode 100644
index 0000000000000000000000000000000000000000..03872ead6887e537c62a775dc26e5b20639977c4
--- /dev/null
+++ b/roles/openvpn-frontend/tasks/main.yml
@@ -0,0 +1,6 @@
+- name: Install Openvpn DNS records
+  file:
+    src: 'openvpn_dns.yml'
+    dest: '/etc/dns/manual/openvpn.yml'
+  notify: reload openvpn DNS config
+
diff --git a/roles/openvpn-frontend/templates/openvpn_dns.j2 b/roles/openvpn-frontend/templates/openvpn_dns.j2
new file mode 100644
index 0000000000000000000000000000000000000000..490a2b514a58557f0327a4efce4d5466e19b08a0
--- /dev/null
+++ b/roles/openvpn-frontend/templates/openvpn_dns.j2
@@ -0,0 +1,5 @@
+{{ domain }}:
+  _:
+{% for h in groups['openvpn'] %}
+    - {{ hostvars[h].ip }}
+{% endfor %}
diff --git a/roles/openvpn/files/50openvpn.firewall b/roles/openvpn/files/50openvpn.firewall
new file mode 100644
index 0000000000000000000000000000000000000000..e1dfe066adfe4deac621d01cb72e3779a6916e1b
--- /dev/null
+++ b/roles/openvpn/files/50openvpn.firewall
@@ -0,0 +1,12 @@
+# Allow incoming connections to port 1194 for both udp and tcp
+allow_port tcp 1194
+allow_port udp 1194
+# there is nothing special with regard to networking here, openvpn is just a process
+# iptables by default in float prevents us from forwarding packets (DROP policy on FORWARD)
+# so we need something in the FORWARD table that says "allow packets to / from this vpn"
+#
+# let vpn hosts reach the internet
+add_rule -A FORWARD -s 10.41.0.0/24 -o eth0 -j ACCEPT
+# allow re/established *inbound* to vpn hosts
+add_rule -A FORWARD -d 10.41.0.0/24 -m state --state RELATED,ESTABLISHED -j ACCEPT
+add_rule -t nat -A POSTROUTING -s 10.41.0.0/24 -o eth0 -j MASQUERADE
diff --git a/roles/openvpn/handlers/main.yml b/roles/openvpn/handlers/main.yml
index 01bae4f81d61780ce59f0e2b0a68d39806f83b17..6fa0f9a0efe77baa4cf2c54a356c4a37a378f22f 100644
--- a/roles/openvpn/handlers/main.yml
+++ b/roles/openvpn/handlers/main.yml
@@ -1,4 +1,11 @@
+---
+
 - name: reload firewall
   systemd:
     name: firewall.service
     state: restarted
+
+- listen: "restart docker-openvpn-openvpn"
+  systemd:
+    name: "docker-openvpn-openvpn.service"
+    state: restarted
diff --git a/roles/openvpn/tasks/main.yml b/roles/openvpn/tasks/main.yml
index 6afdfc9ba79672afac93789256fc311b9a99745a..f34e97041ae6be6b246fa749f5e2eda4589fb656 100644
--- a/roles/openvpn/tasks/main.yml
+++ b/roles/openvpn/tasks/main.yml
@@ -23,3 +23,48 @@
     path: "/srv/leap/shapeshifter-state/obfs4_cert.txt"
     owner: docker-openvpn
     mode: 0600
+
+- name: Create openvpn directory
+  file:
+    path: /etc/openvpn
+    state: directory
+    group: docker-openvpn
+    mode: 0750
+
+- name: Install DH parameters
+  copy:
+    src: "{{ credentials_dir }}/x509/dhparam"
+    dest: /etc/openvpn/dh.pem
+
+- name: Install OpenVPN CA
+  copy:
+    src: "{{ credentials_dir }}/openvpn/ca.pem"
+    dest: /etc/openvpn/ca.pem
+
+- name: Install OpenVPN key
+  copy:
+    src: "{{ credentials_dir }}/openvpn/private_key.pem"
+    dest: /etc/openvpn/private_key.pem
+
+- name: Install OpenVPN cert
+  copy:
+    src: "{{ credentials_dir }}/openvpn/cert.pem"
+    dest: /etc/openvpn/cert.pem
+
+- name: Install firewall config for openvpn
+  copy:
+    src: "50openvpn.firewall"
+    dest: "/etc/firewall/filter.d/50openvpn"
+  notify: "reload firewall"
+
+- name: Install openvpn configs
+  template:
+    src: "{{ item }}"
+    dest: "/etc/openvpn"
+    mode: 0644
+  with_items:
+    - udp.conf
+    - tcp.conf
+    - shapeshifter.conf
+  notify:
+    - "restart docker-openvpn-openvpn"
diff --git a/roles/openvpn/templates/shapeshifter.conf b/roles/openvpn/templates/shapeshifter.conf
new file mode 100644
index 0000000000000000000000000000000000000000..81a3fca68ceef4c52f14d21905071689ff8c7410
--- /dev/null
+++ b/roles/openvpn/templates/shapeshifter.conf
@@ -0,0 +1,33 @@
+mode server
+tls-server
+port 1194
+auth SHA1
+ca /etc/openvpn/ca.pem
+cert /etc/openvpn/cert.pem
+key /etc/openvpn/private_key.pem
+cipher AES-128-CBC
+dev tun
+dh /etc/openvpn/dh.pem
+duplicate-cn
+keepalive 10 30
+local 127.0.0.1
+management 127.0.0.1 1000
+mute-replay-warnings
+mute 5
+proto tcp
+push "dhcp-option DNS {{ ip }}"
+push "redirect-gateway def1"
+push "route-ipv6 2000::/3"
+push "ignore-unknown-option"
+push "block-outside-dns"
+script-security 1
+server-ipv6 2001:db8:123::/64
+server 10.41.0.0 255.255.248.0
+status /var/run/openvpn-status-tcp 10
+status-version 3
+tcp-nodelay
+tls-cipher DHE-RSA-AES128-SHA
+verify-x509-name "CN=UNLIMITED"
+topology subnet
+tun-ipv6
+verb 3
diff --git a/roles/openvpn/templates/tcp.conf b/roles/openvpn/templates/tcp.conf
new file mode 100644
index 0000000000000000000000000000000000000000..436407458ec614750a8a2d3da474947f22492082
--- /dev/null
+++ b/roles/openvpn/templates/tcp.conf
@@ -0,0 +1,31 @@
+mode server
+tls-server
+port 1194
+auth SHA1
+ca /etc/openvpn/ca.pem
+cert /etc/openvpn/cert.pem
+key /etc/openvpn/private_key.pem
+cipher AES-128-CBC
+dev tun
+dh /etc/openvpn/dh.pem
+duplicate-cn
+keepalive 10 30
+local {{ public_ip|default(ip) }}
+mute-replay-warnings
+mute 5
+proto tcp
+push "dhcp-option DNS {{ ip }}"
+push "redirect-gateway def1"
+push "route-ipv6 2000::/3"
+push "ignore-unknown-option"
+push "block-outside-dns"
+script-security 1
+server-ipv6 2001:db8:123::/64
+server 10.41.0.0 255.255.248.0
+status /tmp/openvpn-status-tcp 10
+status-version 3
+tcp-nodelay
+tls-cipher DHE-RSA-AES128-SHA
+verify-x509-name "CN=UNLIMITED"
+topology subnet
+verb 3
diff --git a/roles/openvpn/templates/udp.conf b/roles/openvpn/templates/udp.conf
new file mode 100644
index 0000000000000000000000000000000000000000..155d5dbe250d8845be738ee888b59c9886a201fb
--- /dev/null
+++ b/roles/openvpn/templates/udp.conf
@@ -0,0 +1,32 @@
+mode server
+tls-server
+port 1194
+auth SHA1
+ca /etc/openvpn/ca.pem
+cert /etc/openvpn/cert.pem
+key /etc/openvpn/private_key.pem
+cipher AES-128-CBC
+dev tun
+dh /etc/openvpn/dh.pem
+duplicate-cn
+keepalive 10 30
+local {{ public_ip|default(ip) }}
+mute-replay-warnings
+mute 5
+proto udp
+push "dhcp-option DNS {{ ip }}"
+push "redirect-gateway def1"
+push "route-ipv6 2000::/3"
+push "ignore-unknown-option"
+push "block-outside-dns"
+script-security 1
+server-ipv6 2001:db8:123::/64
+server 10.41.0.0 255.255.248.0
+status /tmp/openvpn-status-udp 10
+status-version 3
+tcp-nodelay
+tls-cipher DHE-RSA-AES128-SHA
+verify-x509-name "CN=UNLIMITED"
+topology subnet
+tun-ipv6
+verb 3
diff --git a/services.yml b/services.yml
index a63898dc9034d544f16fad68013f3a462d5d7ccb..37d4cf64977d1e84e6b3d3ce2cd68fb282464081 100644
--- a/services.yml
+++ b/services.yml
@@ -12,7 +12,7 @@ geoip:
   public_endpoints:
     - name: getmyip
       port: 9001
-      scheme: http  
+      scheme: http
 
 openvpn:
   # these are udp/tcp services, so we don't want to put them behind the reverse proxy
@@ -20,22 +20,31 @@ openvpn:
   #   1) set scheduling_group=frontend,
   #   2) create a 'vpn' group, intended for horizontal scaling of the public vpn nodes,
   #      and run this service there (so you can keep assuming that 'backend' hosts are 'private')
+  #
+  # we want to schedule the openvpn-frontend role on the 'frontend' groups,
+  # there is no openvpn-frontend service, but instead we are customizing the
+  # 'frontend' role
+  #
+  # We do not want to use public_tcp_endpoints for openvpn/shapeshifter, because
+  # that results in haproxy being setup for the port. Reverse proxies are
+  # connection based, and we want to also be able to do UDP, so we do not really
+  # want to have a reverse proxy for openvpn, instead we want to set it up as a
+  # user-facing service, and just rely on DNS for request routing and balancing.
+
   scheduling_group: frontend
   num_instances: 1
-  public_tcp_endpoints:
-    - name: shapeshifterlistener
-      port: 23042
-    - name: openvpn
-      port: 1194
   containers:
     - name: openvpn
       image: registry.0xacab.org/leap/container-platform/openvpn:latest
       ports: [1194, 23042]
+      drop_capabilities: false
+      docker_options: '--cap-add=NET_ADMIN'
       volumes:
         - /srv/leap/shapeshifter-state: /srv/leap/shapeshifter-state
+        - /etc/openvpn: /etc/openvpn
+        - /dev/net: /dev/net
       env:
         # Shapeshifter specific environment variables
         LHOST: "{{ ip }}"
         _CHAP_OPTIONS: --no-syslog
         LOGLEVEL: DEBUG
-
diff --git a/site.yml b/site.yml
index 6dcbcf6cf28631b57104e7902820b931bdea4c9e..b99fb40d15a5bceb27c193b52d0daecbdc97b25c 100644
--- a/site.yml
+++ b/site.yml
@@ -1,5 +1,9 @@
 ---
 - import_playbook: ../../float/playbooks/all.yml
+
 - hosts: openvpn
   roles: [openvpn]
-  
+
+- hosts: frontend
+  roles: [openvpn-frontend]
+