{
  "id": "CB-UBU-PKG-012",
  "standard": "ubuntu",
  "version": "0.1.1",
  "url": "https://baselines.cirsec.org/b/ubuntu/v0.1.1/CB-UBU-PKG-012",
  "json": "https://baselines.cirsec.org/b/ubuntu/v0.1.1/CB-UBU-PKG-012.json",
  "yaml": "https://baselines.cirsec.org/b/ubuntu/v0.1.1/CB-UBU-PKG-012.yml",
  "profiles_effective": [
    "core",
    "server",
    "server-hardened",
    "desktop",
    "desktop-hardened"
  ],
  "license": "Text CC BY 4.0, code Apache-2.0",
  "attribution": "CIRSEC Baselines, https://baselines.cirsec.org",
  "control": {
    "id": "CB-UBU-PKG-012",
    "title": "Keep snap packages refreshed automatically",
    "finding": {
      "title": "Snap packages are not refreshed automatically",
      "description": "Automatic snap refreshes are stopped or held for long, so the snaps keep their known vulnerabilities while APT updates are applied."
    },
    "status": "active",
    "profiles": [
      "core"
    ],
    "risk": {
      "severity": "medium"
    },
    "assessment": "automated",
    "tags": [
      "snap",
      "updates"
    ],
    "applicability": {
      "facts": [
        "snapd"
      ]
    },
    "description": "Snaps are installed and updated by `snapd`, not by APT: unattended-upgrades never touches\nthem. snapd checks the store several times a day (`refresh.timer`) and installs new\nrevisions on its own. An administrator can postpone these refreshes:\n\n- for all snaps, with `snap refresh --hold[=<duration>]` (the `refresh.hold` system option);\n- for some snaps, with `snap refresh --hold[=<duration>] <snap>`.\n\nWithout a duration, the hold lasts forever. The control fails when a hold ends more than\n`{{ max_hold_days }}` days from now, and when the `snapd` service is not enabled.\n",
    "rationale": "Snaps bundle their own libraries: a fix in an Ubuntu library reaches a snap only when the snap\nis rebuilt and refreshed. LXD, Firefox, Thunderbird and many agents ship as snaps, and some of\nthem face the network. An indefinite hold, often set once to get through an incident and then\nforgotten, leaves them vulnerable for good while the APT side of the system looks up to date.\n",
    "impact": "A refresh restarts the services of the refreshed snap (for example the LXD daemon, not its\ncontainers). Use `refresh.timer` to move refreshes into a maintenance window, and holds of\nat most `{{ max_hold_days }}` days to postpone them, rather than indefinite holds.\n",
    "notes": "`snapd` is installed on every Ubuntu release (fact `snapd`); the 20.04 and 22.04 cloud images\nship the `lxd` snap, Ubuntu Desktop ships Firefox and other applications as snaps. A hold that\nhas already ended (for example one set while building an image) passes. Snaps installed from a\nlocal file (`--dangerous`, revisions `x1`, `x2`...) are never refreshed: list them with\n`snap list` and replace them with store snaps or remove them.\n",
    "setting": {
      "location": "snapd system options (snap get system refresh), per-snap holds",
      "default_value": "No hold, snapd.service enabled",
      "recommended_value": "No hold ending more than {{ max_hold_days }} days from now, snapd.service enabled"
    },
    "parameters": [
      {
        "name": "max_hold_days",
        "type": "integer",
        "description": "Longest accepted refresh hold, counted from now, for all snaps or for one snap.",
        "unit": "days",
        "default": 30,
        "minimum": 1,
        "maximum": 90
      }
    ],
    "audit": {
      "manual": "1. `systemctl is-enabled snapd.service` prints `enabled`.\n2. `snap refresh --time` shows no `hold:` line, or a hold ending in at most\n   `{{ max_hold_days }}` days.\n3. `snap list` shows no snap with `held` in the Notes column. For a held snap, read the end of\n   its hold in the `hold` field of\n   `curl -s --unix-socket /run/snapd.socket http://localhost/v2/snaps` (an indefinite hold\n   shows a date about 300 years ahead); it must be at most `{{ max_hold_days }}` days ahead.\n",
      "checks": [
        {
          "id": "snapd-service",
          "engine": "systemd-unit",
          "description": "The snapd service, which runs the automatic refreshes.",
          "spec": {
            "unit": "snapd.service",
            "state": "enabled"
          }
        },
        {
          "id": "holds",
          "engine": "shell",
          "description": "Refresh holds for all snaps and for each snap, read from the snapd API.",
          "spec": {
            "script": "command -v python3 >/dev/null || { echo \"python3 is needed to read the snapd API\"; exit 3; }\n[ -S /run/snapd.socket ] || { echo \"snapd socket missing, snapd is not running\"; exit 1; }\npython3 - \"$CB_MAX_HOLD_DAYS\" <<'EOF'\nimport calendar, json, socket, sys, time\n\ndef get(path):\n    s = socket.socket(socket.AF_UNIX)\n    s.settimeout(30)\n    s.connect(\"/run/snapd.socket\")\n    s.sendall((\"GET %s HTTP/1.0\\r\\nHost: localhost\\r\\n\\r\\n\" % path).encode())\n    data = b\"\"\n    while True:\n        chunk = s.recv(65536)\n        if not chunk:\n            break\n        data += chunk\n    s.close()\n    return json.loads(data.split(b\"\\r\\n\\r\\n\", 1)[1])[\"result\"]\n\ndef until(value):\n    if not value:\n        return None\n    if value == \"forever\":\n        return float(\"inf\")\n    return calendar.timegm(time.strptime(value[:19], \"%Y-%m-%dT%H:%M:%S\"))\n\ntry:\n    holds = [(\"all snaps\", get(\"/v2/snaps/system/conf?keys=refresh\").get(\"refresh\", {}).get(\"hold\"))]\n    holds += [(s[\"name\"], s.get(\"hold\")) for s in get(\"/v2/snaps\")]\nexcept Exception as e:\n    print(\"snapd API error: %s\" % e)\n    sys.exit(3)\nlimit = time.time() + int(sys.argv[1]) * 86400\nbad = 0\nfor name, value in holds:\n    t = until(value)\n    if t is None or t <= time.time():\n        continue\n    days = \"forever\" if t == float(\"inf\") or t > time.time() + 36500 * 86400 else \"%d days\" % ((t - time.time()) // 86400)\n    print(\"%s: held for %s (%s)\" % (name, days, value))\n    bad += t > limit\nprint(\"%d snaps, %d holds longer than %s days\" % (len(holds) - 1, bad, sys.argv[1]))\nsys.exit(1 if bad else 0)\nEOF\n"
          }
        }
      ]
    },
    "remediation": {
      "manual": "1. `systemctl enable --now snapd.service` if it was disabled.\n2. Remove the long holds: `snap refresh --unhold` for all snaps, `snap refresh --unhold <snap>`\n   for one snap. If a refresh must be postponed, use `snap refresh --hold=<duration>` with a\n   duration of at most `{{ max_hold_days }}` days.\n3. To keep refreshes out of business hours, set a window instead, for example\n   `snap set system refresh.timer=sat,02:00-04:00`.\n4. Run `snap refresh` to catch up, during a maintenance window if snap services must not\n   restart now.\n\nThe automated fix enables `snapd.service` and removes every refresh hold, bounded ones\nincluded; it does not run `snap refresh`.\n",
      "effects": {
        "restart": "none",
        "disruption": "low",
        "lockout_risk": false
      },
      "rollback": "Hold the refreshes again with `snap refresh --hold=<duration> [<snap>]`.\n",
      "scripts": [
        {
          "id": "snapd-service",
          "from_check": "snapd-service"
        },
        {
          "id": "holds",
          "engine": "shell",
          "content": "# Remove every hold: the check only fails when one is longer than the limit, and a fresh\n# bounded hold can be set again by the administrator.\nif [ -n \"$(snap get system refresh.hold 2>/dev/null)\" ]; then\n  snap refresh --unhold >/dev/null\nfi\nsnap list 2>/dev/null | awk 'NR > 1 && $NF ~ /(^|,)held(,|$)/ { print $1 }' |\n  while IFS= read -r s; do\n    snap refresh --unhold \"$s\" >/dev/null || exit 1\n  done\n"
        }
      ]
    },
    "references": [
      {
        "title": "Snapcraft documentation - Managing updates",
        "url": "https://snapcraft.io/docs/managing-updates",
        "publisher": "Canonical",
        "kind": "vendor-doc",
        "accessed": "2026-10-19"
      },
      {
        "title": "snap(8), Ubuntu manual pages",
        "url": "https://manpages.ubuntu.com/manpages/noble/man8/snap.8.html",
        "publisher": "Canonical",
        "kind": "vendor-doc",
        "accessed": "2026-10-19"
      },
      {
        "title": "Ubuntu Security documentation",
        "url": "https://documentation.ubuntu.com/security/",
        "publisher": "Canonical",
        "kind": "vendor-doc",
        "accessed": "2026-10-19"
      }
    ],
    "mappings": [
      {
        "framework": "nist-800-53r5",
        "framework_version": "5.1.1",
        "id": "SI-2",
        "relation": "partial",
        "note": "Covers the snap packages, which the APT update controls do not reach."
      }
    ],
    "threats": [
      {
        "framework": "mitre-attack",
        "id": "T1190"
      },
      {
        "framework": "mitre-attack",
        "id": "T1068"
      },
      {
        "framework": "cwe",
        "id": "CWE-1395"
      }
    ],
    "related_controls": [
      {
        "id": "CB-UBU-PKG-007",
        "type": "related"
      },
      {
        "id": "CB-UBU-PKG-010",
        "type": "related"
      }
    ],
    "authors": [
      {
        "name": "Alice",
        "github": "Sn0wAlice"
      }
    ],
    "verified": [
      {
        "date": "2026-10-19",
        "by": "Sn0wAlice",
        "versions": [
          "20.04"
        ],
        "environment": "Proxmox VM, Ubuntu 20.04 cloud image, kernel 5.4.0-216-generic",
        "notes": "Passes on stock (expired image hold, lxd/core20/snapd); forever hold on all snaps and on one snap, 59-day hold on lxd and disabled snapd.service detected, 9-day hold accepted; fix unholds and enables snapd, lxd works after reboot."
      },
      {
        "date": "2026-10-19",
        "by": "Sn0wAlice",
        "versions": [
          "22.04"
        ],
        "environment": "Proxmox VM, Ubuntu 22.04 cloud image, kernel 5.15.0-198-generic",
        "notes": "Passes on stock (lxd/core22/snapd); forever hold on all snaps and on one snap, 59-day hold on lxd and disabled snapd.service detected, 9-day hold accepted; fix unholds and enables snapd, lxd works after reboot."
      },
      {
        "date": "2026-10-19",
        "by": "Sn0wAlice",
        "versions": [
          "24.04"
        ],
        "environment": "Proxmox VM, Ubuntu 24.04 cloud image, kernel 6.8.0-147-generic",
        "notes": "Passes on stock (no snap installed); forever hold on all snaps and on hello-world detected; fix removes both holds."
      },
      {
        "date": "2026-10-19",
        "by": "Sn0wAlice",
        "versions": [
          "26.04"
        ],
        "environment": "Proxmox VM, Ubuntu 26.04 cloud image, kernel 7.0.0-38-generic",
        "notes": "Passes on stock (no snap installed); forever hold on all snaps and on hello-world detected; fix removes both holds."
      }
    ],
    "introduced_in": "0.1.0",
    "created": "2026-10-19",
    "updated": "2026-10-19"
  }
}