Impermanence: A file already exists at /etc/machine-id

When rebuilding and switching my config with impermanence I keep getting this error after a few rebuilds.
I then remove /etc/machine-id but it keeps coming back after a while.
I also saw if for /etc/ssh/ssh_host… , I don’t see it for the directories.
Any suggestions?

...
A file already exists at /etc/machine-id!
Activation script snippet 'persist-files' failed (1)
reloading user units for some-user...
setting up tmpfiles
warning: the following units failed: persist--persist-etc-machine-id-.service

× persist--persist-etc-machine-id-.service - Bind mount or link '/persist/etc/machine-id' to '/etc/machine-id'
     Loaded: loaded (/etc/systemd/system/persist--persist-etc-machine-id-.service; enabled; vendor preset: enabled)
     Active: failed (Result: exit-code) since Sun 2022-07-10 20:19:49 CEST; 3s ago
    Process: 51040 ExecStart=/nix/store/ydjw9khp0kixn9la6pmqsgaaaaaaaaaa-impermanence-mount-file /etc/machine-id /persist/etc/machine-id  (code=exited, status=1/FAILURE)
   Main PID: 51040 (code=exited, status=1/FAILURE)
         IP: 0B in, 0B out
        CPU: 8ms

Jul 10 20:19:49 some-hostname systemd[1]: Starting Bind mount or link '/persist/etc/machine-id' to '/etc/machine-id'...
Jul 10 20:19:49 some-hostname ydjw9khp0kixn9la6pmqsgaaaaaaaaaa-impermanence-mount-file[51040]: A file already exists at /etc/machine-id!
Jul 10 20:19:49 some-hostname systemd[1]: persist--persist-etc-machine-id-.service: Main process exited, code=exited, status=1/FAILURE
Jul 10 20:19:49 some-hostname systemd[1]: persist--persist-etc-machine-id-.service: Failed with result 'exit-code'.
Jul 10 20:19:49 some-hostname systemd[1]: Failed to start Bind mount or link '/persist/etc/machine-id' to '/etc/machine-id'.

I suspect it comes back on reboot, specifically. This is because impermanence’s units are activating too late to properly redirect that file. systemd itself needs it, and will create it with a random value if it’s not present, so a systemd service is kinda by definition too late.

Also, /etc/machine-id actually doesn’t ever need to be modified, so you can install it into place with environment.etc instead, as I do in my config here.

As for actual state that’s needed very early, I use an initrd snippet to get it into place like this.

3 Likes

It certainly happens after every reboot.

Showing the machine-id at other locations (git) is not considered safe?

man machine-id

   This ID uniquely identifies the host. It should be considered
   "confidential", and must not be exposed in untrusted environments, in
   particular on the network. If a stable unique identifier that is tied
   to the machine is needed for some application, the machine ID or any
   part of it must not be used directly. Instead the machine ID should be
   hashed with a cryptographic, keyed hash function, using a fixed,
   application-specific key. That way the ID will be properly unique, and
   derived in a constant way from the machine ID but there will be no way
   to retrieve the original machine ID from the application-specific one.
   The sd_id128_get_machine_app_specific(3) API provides an
   implementation of such an algorithm.

I see the ideal situation as:

  • the machine-id is auto-generated on first boot.
  • a way to move it to /persist or generate it at /persist
  • symlink

The symlink part you did with some other paths. I’m unsure about the second.

  boot.initrd.postMountCommands = lib.mkBefore ''
    ln -snfT /persist/etc/machine-id /etc/machine-id
  '';
1 Like

Hmm, I wasn’t aware that the machine-id was considered confidential… unfortunately, there’s no explanation of why it’s considered confidential. Anyway, you can just use quotes on the value of .source like I did here to make it a symlink to some persisted location.

I switch over from impermanence to chvp’s implementation: https://github.com/chvp/nixos-config/blob/562fcbbcf18703f370beae869529a6ee85704cf2/modules/base/zfs/default.nix
I had to add a few things:

  • make sure all homeLinks are owned by the user
  • symlink machine-id with a little script

Disclaimer: I just(today) did these thing, be carefull to copy. I haven’t enabled rollback yet.

{ config, pkgs, lib, ... }:
let
  cfg = config.ncfg.persist;
in
{
  options =
    {
      ncfg.persist = rec {
        enable = lib.mkEnableOption "a root with explicit opt-in state";
        eraseOnBoot = lib.mkEnableOption "zfs rollback dataset on boot";

        cachePrefix = lib.mkOption {
          type = lib.types.str;
          default = if cfg.enable then "/cache" else "";
        };
        persistPrefix = lib.mkOption {
          type = lib.types.str;
          default = if cfg.enable then "/persist" else "";
        };

        systemLinks = lib.mkOption {
          default = [ ];
          example = [
            { path = "/var/lib/docker"; type = "cache"; }
            { path = "/var/lib/docker/volumes"; type = "persist"; }
          ];
        };
        homeLinks = lib.mkOption {
          default = [ ];
          example = [
            { path = ".cache/nix-index"; type = "cache"; }
            { path = ".config/syncthing"; type = "persist"; }
          ];
        };
        ensureSystemExists = lib.mkOption {
          default = [ ];
          example = [ "/persist/etc/ssh" ];
        };
        ensureHomeExists = lib.mkOption {
          default = [ ];
          example = [ "/persist/home/USER/.ssh" ];
        };

        homeDir = lib.mkOption {
          type = lib.types.path;
          default = "${config.users.users.${config.ncfg.primaryUserName}.home}";
          # default = "/home/${config.ncfg.primaryUserName}";
        };
      };
    };

  config = lib.mkIf cfg.enable {
    # Opt-In State # RPOOL: HOME & ROOT
    # Erase zfs pools on boot
 ### NOTE: HAVEN'T DONE A ROLLBACK YET ###
    boot.initrd.postDeviceCommands = lib.mkIf cfg.eraseOnBoot (lib.mkAfter ''
      zfs rollback -r ${config.ncfg.zfs.rootDataset}@blank
    '');
    # zfs rollback -r ${config.ncfg.zfs.homeDataset}@blank

    system.activationScripts =
      let
        ensureSystemExistsScript = lib.concatStringsSep "\n" (map (path: ''mkdir -p "${path}"'') cfg.ensureSystemExists);
        ensureHomeExistsScript = lib.concatStringsSep "\n" (map
          (path: ''
            mkdir -p "${path}";
            chown ${config.ncfg.primaryUserName}:users ${path};'')
          cfg.ensureHomeExists);
        ensureHomeLinksExistsScript = lib.concatStringsSep "\n" (map
          (location: ''
            mkdir -p "/${location.type}${cfg.homeDir}/${location.path}";
            chown ${config.ncfg.primaryUserName}:users /${location.type}${cfg.homeDir}/${location.path};'')
          cfg.homeLinks);
      in
      {
        ensureMachine-idPersists = {
          text = ''
            FILE=/etc/machine-id
            PERSISTFILE="${config.ncfg.persist.persistPrefix}$FILE"
            if ! [ -L "$FILE" ]; then
              if [ -f "$FILE" ]; then
                echo "regular file $FILE exists."
                if [ -e "$PERSISTFILE" ]; then
                  echo "file $PERSISTFILE exists."
                  TS=$(date +"%Y%m%d_%H%M%S")
                  mv $PERSISTFILE "$PERSISTFILE_$TS"
                fi
                mv $FILE $PERSISTFILE
                ln -sfnT $PERSISTFILE $FILE
              elif [ -e "$FILE" ]; then
                echo "file $FILE exists."
              else
                echo "$FILE does not exist."
                ln -sfnT "$PERSISTFILE" $FILE
              fi
            # else
            #   echo "symbolic link $FILE exists."
            fi
          '';
          deps = [ "agenixMountSecrets" ];
        };
        ensureSystemPathsExist = {
          text = ensureSystemExistsScript;
          deps = [ "agenixMountSecrets" ];
        };
        agenixRoot.deps = [ "ensureSystemPathsExist" ];
        ensureHomePathsExist = {
          text = ''
            mkdir -p ${cfg.homeDir}/
            ${ensureHomeExistsScript}
            ${ensureHomeLinksExistsScript}
          '';
          deps = [ "users" "groups" ];
        };
        agenix.deps = [ "ensureHomePathsExist" ];
      };

    systemd.services =
      let
        makeLinkScript = config: lib.concatStringsSep "\n" (map (location: ''mkdir -p "${location.path}"'') config);
        systemLinksScript = makeLinkScript cfg.systemLinks;
        homeLinksScript = makeLinkScript cfg.homeLinks;
      in
      {
        make-system-links-destinations = {
          script = systemLinksScript;
          after = [ "local-fs.target" ];
          wants = [ "local-fs.target" ];
          before = [ "shutdown.target" "sysinit.target" ];
          conflicts = [ "shutdown.target" ];
          wantedBy = [ "sysinit.target" ];
          serviceConfig = {
            RemainAfterExit = "yes";
            Type = "oneshot";
            UMask = "0077";
          };
          unitConfig = {
            DefaultDependencies = "no";
          };
        };

        make-home-links-destinations = {
          script = homeLinksScript;
          after = [ "local-fs.target" "make-system-links-destinations.service" ];
          wants = [ "local-fs.target" "make-system-links-destinations.service" ];
          before = [ "shutdown.target" "sysinit.target" ];
          conflicts = [ "shutdown.target" ];
          wantedBy = [ "sysinit.target" ];
          serviceConfig = {
            RemainAfterExit = "yes";
            Type = "oneshot";
            User = "${config.ncfg.primaryUserName}";
            Group = "users";
            UMask = "0077";
            WorkingDirectory = "${cfg.homeDir}";
          };
          unitConfig = {
            DefaultDependencies = "no";
          };
        };
      };

    systemd.mounts =
      (map
        (location: {
          what = "/${location.type}${location.path}";
          where = "${location.path}";
          type = "none";
          options = "bind";
          after = [ "local-fs.target" "make-system-links-destinations.service" ];
          wants = [ "local-fs.target" "make-system-links-destinations.service" ];
          before = [ "umount.target" "sysinit.target" ];
          conflicts = [ "umount.target" ];
          wantedBy = [ "sysinit.target" ];
          unitConfig = {
            DefaultDependencies = "no";
          };
        })
        cfg.systemLinks) ++
      (map
        (location: {
          what = "/${location.type}${cfg.homeDir}/${location.path}";
          where = "${cfg.homeDir}/${location.path}";
          type = "none";
          options = "bind";
          after = [ "local-fs.target" "make-home-links-destinations.service" ];
          wants = [ "local-fs.target" "make-home-links-destinations.service" ];
          before = [ "umount.target" "sysinit.target" ];
          conflicts = [ "umount.target" ];
          wantedBy = [ "sysinit.target" ];
          unitConfig = {
            DefaultDependencies = "no";
          };
        })
        cfg.homeLinks);
  };
}
1 Like

Ignore the error, it’s a warning. Your machine-id file will be regenerated on boot and be placed into the persistent location since a symlink will point to it. If you want it to happen ahead of time then simply rm /etc/machine-id and then activate the system configuration. The same thing happens with home-manager if you try to write your .basrhc with it, but you already have a .bashrc in your home directory. Similarly, in that case, you need to rm ~/.bashrc before home-manager can take over. Home-manager has an option to force the replacement of the file, which I enjoy. Impermanence has no such ‘force’ option, but I think it would be a good idea to add.

Good call on the script! I think "$PERSISTFILE_$TS" needs braces for proper expansion, though, which in turn needs to be escaped for Nix, so the line would be:

 mv $PERSISTFILE "''${PERSISTFILE}_''${TS}"