Getting started

LXC support in DebOps

This role focuses only on the LXC configuration itself. You will need to use other DebOps roles to manage additional required subsystems.

The role will configure an internal lxcbr0 bridge for the local Linux Containers, using the lxc-net service. The internal network will have its own DHCP/DNS server with lxc.{{ ansible_domain }} DNS domain by default. You can configure a DNS proxy on the LXC host to be able to access the LXC containers by their DNS names instead of their IP addresses. The debops.dnsmasq and debops.unbound Ansible roles will automatically integrate with the LXC host configuration using Ansible local facts and will generate the configuration necessary to access the lxc.{{ ansible_domain }} DNS domain.

If the LXD support is also configured on the host (detected by the inventory host being included in the [debops_service_lxd] Ansible inventory group), the role will disable and deconfigure any existing lxcbr0 bridge, and will not configure it initially. You should refer to the LXD documentation for details about network bridge management.

Additional bridge network interfaces can be maintained using the debops.ifupdown role. By default the ifupdown role creates the br0 network bridge attached to the external network, which is defined in the LXC configuration as the default network interface to attach the containers, if the internal bridge is disabled.

The containers will use DHCP to get their network interface configuration. You can use debops.dnsmasq or debops.dhcpd Ansible roles to manage a DHCP service on the LXC host or elsewhere in the network.

Unprivileged containers

You can create unprivileged LXC container owned by the root account by using the command:

lxc-create -n <container> -f /etc/lxc/unprivileged.conf \
           -t download -- --dist debian --release stretch --arch amd64

The role will install the lxc-new-unprivileged script which provides an equivalent functionality to the above command. With it, you can create new LXC unprivileged containers by running:

lxc-new-unprivileged <container>

The container will be configured to use subordinate UID/GID range defined by the debops.root_account Ansible role in the /etc/subuid and /etc/subgid databases. Since it's a container owned by root, it will be automatically started on the boot of the host.

Multiple LXC containers that use the same set of subUIDs/subGIDs might be able to access each others' resources in the case of a breakout, since from the perspective of the host their UIDs/GIDs are the same. You might want to consider this in the planning of your environment and use multiple subUID/subGID ranges for different LXC containers or groups of them.

Currently unprivileged LXC containers managed in the DebOps environment should be fairly secure, however you might want to consider enabling AppArmor for increased security against attacks directed at the LXC host.

Privileged containers

You can create a privileged LXC container (default) by using the command as the root account:

lxc-create -n <container> -t debian

To select a different release, use this command:

lxc-create -n <container> -t debian -- -r jessie

You can also specify a different default configuration file, to for example connect the container to specific network bridge:

lxc-create -n <container> -t debian -f /etc/lxc/privileged.conf

Remember that privileged LXC containers are not secure and can modify the LXC host configuration. Don't use privileged containers in production environments, and don't allow untrusted users access to the root account inside of these containers.

The default configuration defined by the role is pretty simple, you can find the configuration of each created LXC container in the /var/lib/lxc/<container>/config configuration file.

LXC containers are managed by systemd

The LXC containers created by the role tasks or by the lxc-new-unprivileged script are managed by systemd, specifically by the lxc@.service unit. This is done because on Debian Stretch using the lxc-stop command directly to stop a container results in a timeout and container processes being forcibly killed by the system. On Debian Stretch and Ubuntu Xenial distributions, the lxc@.service systemd unit is modified by the debops.lxc role to shutdown the container "from the inside" via the lxc-attach command, which results in a properly shut down container.

The containers are configured to not start automatically by the lxc.service unit. Instead, each LXC container has its corresponding lxc@.service instance that will be started by systemd on system boot. On container destruction, either by the debops.lxc role or by the lxc-destroy command, the instance will be disabled automatically.

To start a LXC container using systemd instances, you can issue the command:

systemctl start [email protected]<container>.service

To stop a running LXC container started by systemd, you can execute the command:

systemctl stop [email protected]<container>.service

With this setup, you should avoid using the lxc-* commands that affect the containers directly, unless the container started by the systemd is stopped first. Otherwise the state of the container managed by the systemd instance might get desynchronized.

References and more details about the issues:

SSH access to containers

You can use the command below to start an existing LXC container and prepare SSH access to the root account:

lxc-prepare-ssh <container> [authorized_keys file]

The lxc-prepare-ssh is a custom script installed by the debops.lxc role. It will start the specified container, make sure that OpenSSH service is installed inside, and add the current user's ~/.ssh/authorized_keys contents on the root account inside of the container. The script will check if the ${SUDO_USER} variable is defined in the environment and use that user's ~/.ssh/authorized_keys file as source of SSH public keys. Alternatively, you can specify a custom file with authorized SSH keys to add in the container's /root/.ssh/authorized_keys file.

If the LDAP support is configured on a host and SSH key lookup in LDAP is enabled by the debops.sshd role, the script will look up the current user keys in LDAP directory as well - this ensures that the SSH access is configured even when the SSH public keys are not explicitly defined in the current user's ~/.ssh/authorized_keys file.

After that, the LXC container should be ready to be used remotely, at which point you can use normal DebOps bootstrap playbook and other playbooks to configure it.

Predictable MAC addresses

The lxc-hwaddr-static script can be used to generate predictable, randomized MAC addresses for LXC containers, based on the container name. The script will automatically save the generated MAC addresses in the container configuration files. Multiple network interfaces defined by the lxc.network.type configuration option are supported.

The script can also be used as a "pre-start" LXC hook, to configure static MAC addresses at container start. This requires the container to be restarted for the new static MAC addresses to be used in network interface setup. This usage is enabled by default in DebOps via the common LXC container configuration.

Example inventory

To enable LXC support on a host, it needs to be added to the [debops_service_lxc] Ansible inventory group:

[debops_all_hosts:children]
lxc_hosts
lxc_containers

[debops_service_lxc:children]
lxc_hosts

[lxc_hosts]
lxc-host    ansible_host=lxc-host.example.org

[lxc_containers]
webserver   ansible_host=webserver.example.org

By default, containers will use the lxcbr0 bridge managed by the role, with their own internal subdomain. You can use the debops.ifupdown Ansible role to configure additional network bridges on the LXC host, if you want to attach the containers to the public network.

Remote LXC management without SSH access

Remote LXC containers without SSH access can be accessed indirectly using the lxc_ssh Ansible connection plugin included with DebOps. This requires direct access to the root account on the LXC host and LXC container (even with unprivileged LXC containers), due to the connection plugin limitations.

Example configuration of that connection in the Ansible inventory (variables specified in multiple lines for readability):

[debops_all_hosts:children]
lxc_hosts
lxc_containers

[debops_service_lxc:children]
lxc_hosts

[lxc_hosts]
lxc-host    ansible_host=lxc-host.example.org

[lxc_containers]
webserver    ansible_connection=lxc_ssh ansible_user=root
webserver    ansible_host=lxc-host.example.org
webserver    ansible_ssh_extra_args=webserver

The lxc_ssh connection plugin is unofficial and may not work correctly. Please report any issues, and if you know fixes for them, provide that as well!

Example playbook

If you are using this role without DebOps, here's an example Ansible playbook that uses the debops.lxc role:

---

- name: Manage LXC hosts
  collections: [ 'debops.debops', 'debops.roles01',
                 'debops.roles02', 'debops.roles03' ]
  hosts: [ 'debops_service_lxc' ]
  become: True

  environment: '{{ inventory__environment | d({})
                   | combine(inventory__group_environment | d({}))
                   | combine(inventory__host_environment  | d({})) }}'

  roles:

    - role: resolvconf
      tags: [ 'role::resolvconf', 'skip::resolvconf' ]
      resolvconf__enabled: True

    - role: root_account
      tags: [ 'role::root_account', 'skip::root_account' ]

    - role: apt_preferences
      tags: [ 'role::apt_preferences', 'skip::apt_preferences' ]
      apt_preferences__dependent_list:
        - '{{ lxc__apt_preferences__dependent_list }}'

    - role: ferm
      tags: [ 'role::ferm', 'skip::ferm' ]
      ferm__dependent_rules:
        - '{{ lxc__ferm__dependent_rules }}'

    - role: python
      tags: [ 'role::python', 'skip::python', 'role::lxc' ]
      python__dependent_packages3:
        - '{{ lxc__python__dependent_packages3 }}'
      python__dependent_packages2:
        - '{{ lxc__python__dependent_packages2 }}'

    - role: sysctl
      tags: [ 'role::sysctl', 'skip::sysctl' ]
      sysctl__dependent_parameters:
        - '{{ lxc__sysctl__dependent_parameters }}'

    - role: lxc
      tags: [ 'role::lxc', 'skip::lxc' ]

# If a host has 'debops.dnsmasq' or 'debops.unbound' roles configured, execute
# its playbook in case that configuration applied by the 'lxc' role needs to be
# applied to 'dnsmasq' or 'unbound' services. This should ensure that the
# '*.lxc' subdomain for internal LXC containers is resolvable on the LXC host.
#
# If the host is not in the Ansible inventory groups required by the
# 'dnsmasq.yml' or the 'unbound.yml' playbooks, this should not impact
# anything.

- import_playbook: 'dnsmasq.yml'

- import_playbook: 'unbound.yml'

Ansible tags

You can use Ansible --tags or --skip-tags parameters to limit what tasks are performed during Ansible run. This can be used after a host was first configured to speed up playbook execution, when you are sure that most of the configuration is already in the desired state.

Available role tags:

role::lxc
Main role tag, should be used in the playbook to execute all of the role tasks as well as role dependencies.
role::lxc:containers
Execute tasks that manage LXC containers.
role::lxc:net
Manage internal LXC network configuration.
role::lxc:dnsmasq
Manage the dnsmasq instance of the internal LXC network.

Other resources

List of other useful resources related to the debops.lxc Ansible role: