On 06/23/2015 12:59 PM, Erwan David wrote:
> Note that I use policy-rc.d to check whether the encrypted disk is
> mounted for the daemons that need it (it allows not to change the init
> files)

That works? policy-rc.d should only affect invoke-rc.d, which shouldn't
be relevant at boot, but only in maintainer scripts. (AFAIK at least.)

> For what I need to know : I have a headless machine with an encrypted disk.
> I cannot ask the password on console, so
> 1) at boot I do not mount the encrypted disk, and start a minimal set
> of daemons, among them the ssh daemon.
> 
> 2) I ssh to the machine then mount encrypted disk and start remaining
> daemons.
> 
> How can I do this with systemd ?

This is a great question because it presents a nice little problem that
covers quite a few of topics regarding systemd. I've sat down and
solved your little problem from a systemd perspective, and hopefully my
solution will help you in understanding how systemd works.

First of all a couple of very basic explanations: what is a unit? A
unit is anything that systemd manages and/or monitors. Relevant for
our case are the types service, target and mount, but systemd supports
more than that (see man systemd.unit and the references therein for
more details).

A service unit is probably the simplest to understand, it corresponds
to that which was previously provided by /etc/init.d scripts. I'm not
going to go into much detail here about that.

A mount unit represents a mount in the system. If you manually mount
something via the mount(8) command, systemd will not interfere, but it
will synthesize a dynamic mount unit for it. So for example, if you
have:
mkdir /mnt/a /mnt/b
mount --bind /mnt/a /mnt/b
Then you can also umount /mnt/b via the command:
systemctl stop mnt-b.mount
(umount /mnt/b will continue to work, of course)

For manual mounts this is not really that relevant (I suspect even most
systemd developers will use plain old umount in that case), but mount
units are the method systemd uses to handle /etc/fstab: for every entry
in /etc/fstab a mount unit is generated, that's how /etc/fstab is
integrated into the boot process. (See below for further details.)

A target is in some sense the simplest kind of unit: it can only have
dependencies (and a description), but nothing else. You can achieve the
same thing with a dummy /bin/true service unit. Targets are useful for
grouping things together and also for providing synchronization points.
For example, as is documented in man 7 bootup, there's a target called
local-fs.target that has the semantics that every local filesystem
mount in /etc/fstab will be ordered before it, and so that every
service that orders after it can be sure that local filesystems are
already mounted at that point. (Most services are implicitly ordered
after local-fs.target because by default everything is ordered after
basic.target, which itself is ordered after local-fs.target. You can
override these kind of things if you need to, however.)



How does systemd boot up a system? After doing some very basic
initialization (such as setting the hostname from /etc/hostname and
mounting some essential kernel filesystems such as /proc and /sys), it
will try to start a specific unit, either the default (called
'default.target') or any unit specified on the kernel command line via
the option systemd.unit=XXX. default.target is a symlink to
graphical.target on Debian, but that can be overridden. (If you don't
have a GUI installed, graphical.target is equivalent to
multi-user.target, in analogy to the runlevels 2-5 on Debian with
sysvinit.) graphical.target itself depends on a lot of things, and thus
all services required for system startup are automatically pulled in.



Now how does one solve the problem you have? There are multiple ways to
do so, but for the sake of simplicity I'm going to show just one, the
one I personally prefer (YMMV).

The basic outline would be this:

 - tell systemd to NOT automatically decrypt the drive at boot and to
   NOT automatically mount it
 - create a new target unit that will serve as a new boot target, and
   it will only contain ssh + syslog as services (beyond the very basic
   early-boot things that should always be there)
 - create a second target unit that will pull in the encrypted drive
   and the mount and then ulitmately start the original
   multi-user.target, thus pulling in all other installed daemons on
   the system - that second target will be started manually by you
   after you log in and decrypt the drive


Let's begin. First of all you need to add your drive to /etc/crypttab.
I've done this in a KVM and the entry looks like this:
crypto /dev/vda5 none luks,noauto
(This means that /dev/vda5 is the device with the encrypted LUKS data
on it and /dev/mapper/crypto will be the name of the virtual device
with the plain text data.)
Note that here there is a 'noauto' setting, which means that the drive
should NOT be decrypted automatically at boot (otherwise systemd would
wait for you to enter the password on the console before even mounting
the local filesystems, so it would hang...).

What happens internally here is that systemd uses a so-called generator
to dynamically transform the contents /etc/crypttab into systemd units.
In this case, it generates a service for each line, in our case it will
generate systemd-cryptsetup@crypto.service. If we had not put noauto
here, the service would be started automatically at boot, asking you
for your password at the console. But with 'noauto' here, the service
will be synthesized by the generator, but not started automatically.

You can now see if systemd picks up on that crypttab entry be running
systemctl daemon-reload
That does the following:
  - re-runs all generators (for reading /etc/crypttab, /etc/fstab, ...)
  - re-reads all static and generated unit files on the disk
  => so every time you change something, you need to run the above
     command for systemd to pick it up
You can see that the unit was detected by systemd via:
systemctl status systemd-cryptsetup@crypto.service
But it should also tell you it's inactive (i.e. hasn't been started so
far).

Now we add the entry for the filesystem in /etc/fstab:
/dev/mapper/crypto /srv ext4 rw,noauto 0 0

Here we also have a noauto setting to make sure it's not automatically
mounted at boot. This is really important here, since this is a local
filesystem and systemd considers any local filesystem that couldn't be
mounted at boot to be a fatal error. So if you leave out the noauto
here, systemd will wait 90s by default for the device to appear at boot
and (since it won't appear) will stop the boot process and start an
emergency shell instead. With a headless system that's a really bad
thing to happen, which is why you should definitely put 'noauto' here.
Then systemd won't automatically try to mount it at boot at all.




Now that that's done, let's create our target unit for booting the
system with just a minimal set of daemons. We don't want to interfere
with the early-boot process, so we only want to replace the default
graphical.target, but nothing else.

When editing static units, you should know that systemd knows two
directories where those may be stored: /lib/systemd/system and
/etc/systemd/system. /lib/systemd/system is for things that have been
packaged, whereas /etc/systemd/system is the territory of the
administrator. If files with the same name exist in both directories,
the file in /etc/systemd/system completely overrides the file in
/lib/systemd/system.

So in this case, we want to edit things in /etc/systemd/system. Let's
create a new file /etc/systemd/system/before-decrypt.target. That file
is supposed to be our new default target, so let's put in the
following:
[Unit]
Description=System before Decryption
Requires=basic.target
Conflicts=rescue.service rescue.target
After=basic.target rescue.service rescue.target
AllowIsolate=yes

The contents (apart from Description= and Documentation=) is identical
to the default /lib/systemd/system/multi-user.target. (graphical.target
just pulls that in and since we have a headless system, let's skip the
extra indirection.)

To understand this file, we need to talk systemd dependencies for a
moment. There are a lot of different types of dependencies in systemd,
but in this case you need to understand only a couple (those are also
the most important ones):

 - Requires: if unit A requires the unit B, it means that if A is to
   be started, B also has to be started (and if B fails to start, A
   will not start either). Also, if B is to be stopped, A will also
   be stopped. (Note that this only affects explicit actions, so A
   would not be stopped if B segfaults and just dies unexpectedly,
   unless you tell systemd to do that with an additional setting.)

 - Wants: if unit A wants the unit B, it means that if A is to be
   started, B also has to be started; but A will be started regardless
   of whether A fails to start or not (or even exists or not) - and
   A will not be stopped if B is stopped. It's a weaker version of
   Requires

 - Conflicts: if unit B conflicts with unit B, unit B will be stopped
   if unit A is to be started and vice-versa

 - Before/After: declares ordering within a single transaction. Note
   that ordering is orthogonal in systemd: Requires/Wants do NOT imply
   ordering dependencies, those have to be made explicitly. (Note that
   for targets Requires/Wants do sometimes result in implicit ordering,
   see below for details; but all other unit types don't have that
   logic, so service A requiring service B will NOT lead to an ordering
   between them, unless you also specify that explicitly.)

So in the case of our before-decrypt.target, we have the following
dependencies:

  - Requires=basic.target
    This means that the basic system setup should be pulled in. This
    is essential; if you leave that out the most basic startup things
    will not be started if you try to boot into before-decrypt.target.

  - Conflicts=rescue.service rescue.target
    If you were dropped in an emergency shell at boot (or requested it
    explicitly over the command line), this conflict will tell systemd
    that once you do continue booting from the emergency shell, it
    should stop the emergency shell automatically.

  - After=basic.target rescue.service rescue.target
    Make sure that before-decrypt.target is started only after the very
    basic system initialization is complete.

But wait a minute: we copied this unit from multi-user.target, where
all the non-GUI services should all be running... Where are they? This
unit file appears to only pull in basic.target and nothing else.

Well, since you don't want to modify a unit file every time you install
a new daemon on your system, systemd provides ways to add additional
dependencies to units dynamically via symlinks.

There are two directories for multi-user.target:
/lib/systemd/system/multi-user.target.wants
/etc/systemd/system/multi-user.target.wants

Each directory contains symlinks to unit files that are automatically
added as Wants= type dependencies to the target (you can also have
.requires). The directory in /lib contains all native systemd services
that shouldn't be disabled by the administrator (and systemctl disable
won't work on them although you could manually do something), while the
latter contains all service that can easily be disabled by the
administrator (those include SSH, cron, MTAs, web servers, ...).

Typically one wants to still enable all units from the /lib directory
also in our target, so let's do the following:
mkdir /etc/systemd/system/before-decrypt.target.wants
cd /etc/systemd/system/before-decrypt.target.wants
for i in /lib/systemd/system/multi-user.target.wants/* ; do
   ln -s /lib/systemd/system/$(basename $i) .
done
Then we additionally want to enable SSH and syslog (but no other
services):
cd /etc/systemd/system/before-decrypt.target.wants
ln -s /etc/systemd/system/syslog.service .
ln -s /lib/systemd/system/ssh.service .

A couple of notes:

 - we take syslog.service from /etc because I don't know what your
   favorite syslog daemon is and syslog.service is always a symlink
   to the current one (although you may have to force it manually
   because in Jessie there's a Debian packaging bug if you change
   syslog daemons that the symlink doesn't get updated)

 - both syslog and ssh are native services, which is why that works
 
 - you can also enable additional services you might need in the same
   way. There's no cron nor atd started here (because those might
   require the encrypted partition to be mounted - or not, depending
   on your system) and also no MTA

 - note that these symlinks only work if you have native systemd
   service files. You can also enable init scripts, but then you have
   to explicitly specify Wants=[INITSCRIPTNAME].service, e.g.
   Wants=exim4.service - since the service files for init scripts are
   dynamically generated and you thus can't easily just create a simple
   symlink (in the default case they are activated because systemd's
   generator looks for /etc/rcX.d/S**INITSCRIPTNAME symlinks and then
   creates Wants= dependencies for runlevelX.target dynamically - and
   all runlevel[2-5].target are symlinks to graphical.target)

Then we should be nearly ready for a test boot to see if our minimal
system boots. To summarize what we have done so far:

 - modify /etc/crypttab and /etc/fstab to add all required entries with
   the 'noauto' option
 - create /etc/systemd/system/before-decrypt.target (see above for
   the contents)
 - create directory /etc/systemd/system/before-decrypt.target.wants and
   symlink all services in multi-user.target that we need before the
   partition is decrypted

As a final step we can now make our new target the default boot target:

systemctl set-default before-decrypt.target

Now let's reboot the system. It will come up again and hopefully the
following will have happened:

 - it didn't hang at boot (otherwise check noauto options)
 - SSH was started
 - other system daemons that were not included in the list we activated
   were NOT started (e.g. no cron/atd/exim4)

Now for the final step: we want to be able to decrypt the partition and
enter the password. So let's create a file
/etc/systemd/system/decrypt.target that contains the following:

[Unit]
Description=Decrypted System
Requires=before-decrypt.target
After=before-decrypt.target
Conflicts=systemd-ask-password-console.path 
systemd-ask-password-console.service systemd-ask-password-plymouth.path 
systemd-ask-password-plymouth.service
Requires=systemd-cryptsetup@crypto.service srv.mount start-full-system.service

The interesting settings in that file are:

 - Conflicts= lots of ask-password stuff:
   This is to work around a quirk in systemd's handling of asking
   passwords for encrypted partitions. See footnote [1] for an
   explanation if you are really interested.

 - Requires=...: this is the meat of the matter. Here we make sure we
   pull in
      - the service that decrypts the partition
      - the mount for /srv
      - and an additional service (see below) that will cause the rest
        of the daemons to start (start-full-system.service)

This means that when one then starts this target, it will cause the
partition to be decrypted, the filesystem to be mounted and then all
daemons to be started.

Let's look at start-full-system.service:
[Unit]
Description=Start full system after decryption
After=decrypt.target

[Service]
Type=oneshot
ExecStartPre=/bin/systemctl is-active --quiet decrypt.target
ExecStart=/bin/systemctl --no-block start multi-user.target

The unit calls systemctl to start multi-user.target. The reason this
is done is because of ordering: we only want to start daemons once the
filesystem is mounted. But we don't want to add an explicit After=
dependency to each daemon (or list every daemon as a Before= depdency
here), so instead of pulling in multi-user.target directly in
decrypt.target and adding all those dependencies, we simply create a
small service here that is ordered AFTER decrypt.target that causes
systemd to start multi-user.target.

A word on service ordering: unless you set DefaultDependencies=no, a
target will always be ordered AFTER all of its other dependencies
(Requires=, Wants=, Conflicts=, ...). However, if a service already has
an explicit ordering dependency, this will not occur. So what we have
here is the following ordering:

decrypt.target has an implicit
  After=systemd-cryptsetup@crypto.service srv.mount
BUT because start-full-system.service is ordered relative to
decrypt.mount explicitly already, it doesn't get an implicit ordering.

So that means we have the following order w.r.t. those 4 units:

   systemd-cryptsetup@crypto.service             srv.mount
                  |                                  |
                  \-------------------+--------------/
                                      |
                                      v
                                decrypt.target
                                      |
                                      v
                            start-full-system.service

(In principle, one could also order srv.mount after the cryptsetup
service, but that is unnecessary, since mount units in systemd will
always wait for the corresponding devices to appear, so an explicit
ordering is not required here.)

If you are wondering what the ExecStartPre= is for: it's to make sure
that decrypt.target was successful before it continues. Otherwise, if
decrypting and/or mounting fails, it would still be executed, so add
an additional check that it only starts if decrypting was
successful.[2]

As a final thing w.r.t. start-full-system.service, let's talk about
the --no-block here: it causes systemctl to enqueue the start job for
multi-user.target but will return immediately. The reason for that is
that systemd serializes transactions to a certain extent, so that not
using --no-block here would cause the system to deadlock because
systemctl would wait for the multi-user.target transaction to finish
but systemd wouldn't begin the transaction until the transaction
containing start-full-system.service would be finished. (It's generally
a very bad idea to call systemctl without --no-block in service files.)
However, this means that the starting of the daemons will be initiated
by starting decrypt.target, but it will be done in the background.





Once this is done, let's do a systemctl daemon-reload to make sure
systemd has picked up the changes and now we can try to see if it
works:

systemctl start decrypt.target

You will now be asked for the encrypted partition's password. Once
you've entered that, the filesystem will be mounted, systemctl start
will return you to the command line and in the background all daemons
will be started automatically.



You can reboot the system, log in per SSH again, and use the command
again - and it should work.


Now wait a minute: how does this password prompt work exactly?

 - systemd-cryptsetup@XXX.service will tell systemd that it needs a
   password to decrypt the drive

 - systemd has to ways of getting to such a passowrd: either via a
   so-called 'ask-password agent' (typically used at boot or if the
   administrator plugs in a known encrypted drive dynamically) - or,
   if the password is asked as part of a transaction that was triggered
   via systemct, systemctl will ask the password directly.

So this means that simply telling systemd to decrypt the drive will
then lead to a direct password prompt at that point. So in the end, one
needs only to enter one single command, then enter the password.
Everything will be decrypted and all services will be started then.

Neat, huh?



Summary of what we did:

 - adjust /etc/crypttab and /etc/fstab : use noauto!

 - create /etc/systemd/system/before-decrypt.target

   [Unit]
   Description=System before Decryption
   Requires=basic.target
   Conflicts=rescue.service rescue.target
   After=basic.target rescue.service rescue.target
   AllowIsolate=yes

 - enable required services for before-decrypt.target

   mkdir /etc/systemd/system/before-decrypt.target.wants
   cd /etc/systemd/system/before-decrypt.target.wants
   for i in /lib/systemd/system/multi-user.target.wants/* ; do
      ln -s /lib/systemd/system/$(basename $i) .
   done
   ln -s /etc/systemd/system/syslog.service .
   ln -s /lib/systemd/system/ssh.service .

 - create /etc/systemd/system/decrypt.target

   [Unit]
   Description=Decrypted System
   Requires=before-decrypt.target
   After=before-decrypt.target
   Conflicts=systemd-ask-password-console.path 
systemd-ask-password-console.service systemd-ask-password-plymouth.path 
systemd-ask-password-plymouth.service
   Requires=systemd-cryptsetup@crypto.service srv.mount 
start-full-system.service

 - create /etc/systemd/system/start-full-system.service

   [Unit]
   Description=Start full system after decryption
   After=decrypt.target

   [Service]
   Type=oneshot
   ExecStartPre=/bin/systemctl is-active --quiet decrypt.target
   ExecStart=/bin/systemctl --no-block start multi-user.target

 - reboot

 - to decrypt: systemctl start decrypt.target, enter your password at
   the prompt



A couple of comments:


 1. This can be extended to multiple encrypted drives and also multiple
    mounts. If you want to see what unit name systemd wants to use for
    a given mount point (so you know what to set as Requires= in
    decrypt.target), you can use 'systemctl status /mount/point' to see
    how systemd mangles that into a unit name

 2. If you install additional daemons, Debian will hook them up in
    multi-user.target (either directly if they are native or indirectly
    if they are sysv scripts), so if you install something that should
    already be started before decryption, you need to manually add it.

    On the other hand, since we don't modify multi-user.target itself,
    any new daemon will always be hooked up there automatically, so
    that if you install something, it will automatically be part of the
    start-only-when-decrypted logic.[3]

 3. If you have systemd installed, it's recommended (but not strictly
    required) to have dbus installed. If you have, note that dbus is
    the only service (other than systemd's internal ones and some
    plymouth stuff that's not relevant for headless systems) that hooks
    up in /lib/systemd/system/multi-user.target.wants (and not in
    /etc, as all other services do!), and if it's installed it should
    probably be activated already for the minimal system, so if you
    install dbus only after copying all the symlinks to
    before-decrypt.target.wants (see above), then you should copy that
    one manually.

 4. If you storage stack is a bit more complicated (e.g. you use LVM
    on top of the encryption), you may or may not need to manually add
    some services to activate that once the drive is decrypted. It's
    definitely possible, and might even work out of the box (I haven't
    tried it), but you may have to play around a bit there.
 
 5. If you truly have a headless system, you should test this in a VM
    first, especially when it comes to the boot process.



So there it is: how to solve your particular problem with systemd on
Jessie. I hope I could use this use-case to illuminate a couple of
concepts in systemd and give you enough information so that you have a
good starting point into the topic.


Regards,
Christian


[1] The Conflicts= is required since systemd's own service to notify
    root of password prompts via wall(1) has the following command:
    /bin/systemctl stop [... all units listed in Conflicts= here ...]
    This is fine because it's typically started dynamically when the
    administrator plugis in a drive dynamically or so (and is not
    active during boot, there the other services are active, which
    in turn don't really work after boot, that's why they are stopped).
    The problem here is that we start this from within a transaction,
    causing systemd to enqueue the stop of the units until after the
    transaction for decrypting is done - but then we have a deadlock
    again. Using Conflicts= here will simply tell systemd to always 
    stop those services as part of the transaction to start
    decrypt.service, so nothing will deadlock.

[2] One could also turn it around and NOT make decrypt.target depend
    on start-full-system.service, but the other way around (still with
    Requires=), then the ExecStartPre would not be necessary, and one
    could do systemctl start start-full-system.service... That's
    probably a matter of taste...

[3] There are of course other options of overriding this; for example
    one could leave the standard target alone, just override the
    services for very few daemons and then hook those up with a new
    target. There are advantages and disadvantages with either method.

Attachment: signature.asc
Description: OpenPGP digital signature

Reply via email to