Operating System Past End of Security Support
Checks whether the Linux distribution release still receives free security updates.
- Why It Matters
- Once a release reaches end of life, newly discovered vulnerabilities are never fixed. Every later CVE in the kernel, OpenSSL or the web server stays open forever.
- How It Detects
- Reads ID and VERSION_ID from /etc/os-release and looks them up in a dated table of free vendor security-support dates, including Debian LTS (table date 2026-10-13). Releases older than the oldest table row for their distribution fail. Releases within 90 days of end of life also fail, so there is time to plan the upgrade.
- How to Fix
- Plan an upgrade to a supported release (for example do-release-upgrade on Ubuntu, or a fresh install and migration). Paid extended support (Ubuntu Pro, RHEL ELS) is an alternative while migrating.
- How to Verify
cat /etc/os-release and compare with the vendor lifecycle page.- CVSS v3.1 Vector
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H- Control Category
- Patch and Vulnerability Management: CIS Controls v8 7, 2; NIST CSF 2.0 ID.RA, PR.PS
- Fix Effort
- High
- CIS Controls v8 Safeguard
- 2.2
- NIST CSF 2.0 Subcategory
- ID.RA-01, PR.PS-02
- CWE
- CWE-1104
- OWASP Top 10
- A06:2021
- NIST SP 800-53 Rev. 5
- SA-22, SI-2
- Benchmark Topic
- Supported operating system versions (topic only, no benchmark text)
- References
- Ubuntu release cycle
Debian releases and LTS
Red Hat Enterprise Linux life cycle
endoflife.date (community-maintained lifecycle data)
Source Code
The complete file that defines this check. Run it with openvapt scan --only HOST-OS-001, or read it offline with openvapt explain HOST-OS-001.
internal/checks/host/os_patch.go · Lines 1-221View on GitHub at This Commit
| 1 | package host |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "fmt" |
| 6 | "regexp" |
| 7 | "strconv" |
| 8 | "strings" |
| 9 | "time" |
| 10 | |
| 11 | "github.com/openvapt/openvapt/internal/check" |
| 12 | "github.com/openvapt/openvapt/internal/checks/collect" |
| 13 | "github.com/openvapt/openvapt/internal/sys" |
| 14 | ) |
| 15 | |
| 16 | // eol maps "ID VERSION_ID" to the date standard (free) security support ends. |
| 17 | // Sources: each vendor's published lifecycle page, cited in the check refs. |
| 18 | // The table is dated; a release newer than this table may be missing, while a |
| 19 | // release older than the oldest row of its distribution is always past end of |
| 20 | // support and fails. Every row was re-verified on 2026-10-13 against the |
| 21 | // vendors' published schedules (Canonical, Debian, Red Hat, CentOS, Rocky, |
| 22 | // AlmaLinux, Oracle, Fedora, Amazon) as collected by endoflife.date, with the |
| 23 | // Fedora 44 date from Fedora's own schedule (fedorapeople.org f-44 key |
| 24 | // tasks). Fedora dates are provisional until the release two versions later |
| 25 | // ships; re-check them when the table is next refreshed. |
| 26 | var eol = map[string]string{ |
| 27 | "ubuntu 16.04": "2021-04-02", "ubuntu 18.04": "2023-05-31", "ubuntu 20.04": "2025-05-31", |
| 28 | "ubuntu 20.10": "2021-07-22", "ubuntu 21.04": "2022-01-20", "ubuntu 21.10": "2022-07-14", |
| 29 | "ubuntu 22.04": "2027-06-01", "ubuntu 22.10": "2023-07-20", "ubuntu 23.04": "2024-01-20", |
| 30 | "ubuntu 23.10": "2024-07-12", "ubuntu 24.04": "2029-05-31", "ubuntu 24.10": "2025-07-10", |
| 31 | "ubuntu 25.04": "2026-01-17", "ubuntu 25.10": "2026-07-01", "ubuntu 26.04": "2031-05-31", |
| 32 | "debian 9": "2022-06-30", "debian 10": "2024-06-30", "debian 11": "2026-08-31", "debian 12": "2028-06-30", "debian 13": "2030-06-30", |
| 33 | // CentOS Linux 7 and 8; CentOS Stream 9 reports ID centos as well. |
| 34 | "centos 7": "2024-06-30", "centos 8": "2021-12-31", "centos 9": "2027-05-31", "centos 10": "2030-05-31", |
| 35 | "rhel 7": "2024-06-30", "rhel 8": "2029-05-31", "rhel 9": "2032-05-31", "rhel 10": "2035-05-31", |
| 36 | "rocky 8": "2029-05-31", "rocky 9": "2032-05-31", "rocky 10": "2035-05-31", |
| 37 | "almalinux 8": "2029-05-31", "almalinux 9": "2032-05-31", "almalinux 10": "2035-05-31", |
| 38 | "ol 7": "2024-12-31", "ol 8": "2029-07-31", "ol 9": "2032-06-30", "ol 10": "2035-06-30", |
| 39 | "fedora 39": "2024-11-26", "fedora 40": "2025-05-13", "fedora 41": "2025-12-15", "fedora 42": "2026-05-27", |
| 40 | "fedora 43": "2026-12-09", "fedora 44": "2027-06-02", |
| 41 | "amzn 2018.03": "2023-12-31", "amzn 2": "2026-06-30", "amzn 2023": "2029-06-30", |
| 42 | } |
| 43 | |
| 44 | // EOLTableDate is printed in reports so readers know how fresh the table is. |
| 45 | const EOLTableDate = "2026-10-13" |
| 46 | |
| 47 | // eolWarnDays is how close to end of support a release starts to fail, so |
| 48 | // there is time to plan the upgrade. |
| 49 | const eolWarnDays = 90 |
| 50 | |
| 51 | // majorOnly lists distributions whose support depends on the major version. |
| 52 | var majorOnly = map[string]bool{"centos": true, "rhel": true, "rocky": true, "almalinux": true, "ol": true, "debian": true} |
| 53 | |
| 54 | // osEOL looks up one release. It returns the end date, whether the release |
| 55 | // is past (or within eolWarnDays of) end of support, and whether the result |
| 56 | // is known. A release older than every row for its distribution is reported |
| 57 | // as past end of support without a date. |
| 58 | func osEOL(id, ver string, now time.Time) (end string, bad, known bool, daysLeft int) { |
| 59 | if majorOnly[id] { |
| 60 | ver = strings.SplitN(ver, ".", 2)[0] |
| 61 | } |
| 62 | if e, ok := eol[id+" "+ver]; ok { |
| 63 | d, err := time.Parse("2006-01-02", e) |
| 64 | if err != nil { |
| 65 | return "", false, false, 0 |
| 66 | } |
| 67 | left := d.Sub(now) |
| 68 | return e, left < eolWarnDays*24*time.Hour, true, int(left.Hours() / 24) |
| 69 | } |
| 70 | // Amazon Linux 1 used year.month versions; 2018.03 was the last, so any |
| 71 | // earlier one (2017.09, 2016.09, ...) is past end of support. Comparing |
| 72 | // against the oldest row would fail here, because "2" sorts before them. |
| 73 | if id == "amzn" && amznYear.MatchString(ver) && versionLess(ver, "2018.03") { |
| 74 | return "", true, true, 0 |
| 75 | } |
| 76 | oldest := "" |
| 77 | for k := range eol { |
| 78 | if rid, rv, _ := strings.Cut(k, " "); rid == id && (oldest == "" || versionLess(rv, oldest)) { |
| 79 | oldest = rv |
| 80 | } |
| 81 | } |
| 82 | if oldest != "" && ver != "" && versionLess(ver, oldest) { |
| 83 | return "", true, true, 0 |
| 84 | } |
| 85 | return "", false, false, 0 |
| 86 | } |
| 87 | |
| 88 | var amznYear = regexp.MustCompile(`^20[01][0-9]\.[0-9]{2}$`) |
| 89 | |
| 90 | // versionLess compares dotted numeric versions ("16.04" < "18.04", "9" < "10"). |
| 91 | // A non-numeric component makes the comparison false. |
| 92 | func versionLess(a, b string) bool { |
| 93 | as, bs := strings.Split(a, "."), strings.Split(b, ".") |
| 94 | for i := 0; i < len(as) || i < len(bs); i++ { |
| 95 | var x, y int |
| 96 | if i < len(as) { |
| 97 | n, err := strconv.Atoi(as[i]) |
| 98 | if err != nil { |
| 99 | return false |
| 100 | } |
| 101 | x = n |
| 102 | } |
| 103 | if i < len(bs) { |
| 104 | n, err := strconv.Atoi(bs[i]) |
| 105 | if err != nil { |
| 106 | return false |
| 107 | } |
| 108 | y = n |
| 109 | } |
| 110 | if x != y { |
| 111 | return x < y |
| 112 | } |
| 113 | } |
| 114 | return false |
| 115 | } |
| 116 | |
| 117 | func init() { |
| 118 | check.Register(&check.Check{ |
| 119 | ID: "HOST-OS-001", |
| 120 | Title: "Operating System Past End of Security Support", |
| 121 | Domain: check.DomainHost, |
| 122 | Severity: check.High, |
| 123 | CVSS: "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H", |
| 124 | Summary: "Checks whether the Linux distribution release still receives free security updates.", |
| 125 | Rationale: "Once a release reaches end of life, newly discovered vulnerabilities are never fixed. Every later CVE in the kernel, OpenSSL or the web server stays open forever.", |
| 126 | Detection: "Reads ID and VERSION_ID from /etc/os-release and looks them up in a dated table of free vendor security-support dates, including Debian LTS (table date " + EOLTableDate + "). Releases older than the oldest table row for their distribution fail. Releases within 90 days of end of life also fail, so there is time to plan the upgrade.", |
| 127 | Fix: "Plan an upgrade to a supported release (for example do-release-upgrade on Ubuntu, or a fresh install and migration). Paid extended support (Ubuntu Pro, RHEL ELS) is an alternative while migrating.", |
| 128 | Verify: "cat /etc/os-release and compare with the vendor lifecycle page.", |
| 129 | Refs: []check.Ref{ |
| 130 | {Title: "Ubuntu release cycle", URL: "https://ubuntu.com/about/release-cycle"}, |
| 131 | {Title: "Debian releases and LTS", URL: "https://wiki.debian.org/LTS"}, |
| 132 | {Title: "Red Hat Enterprise Linux life cycle", URL: "https://access.redhat.com/support/policy/updates/errata"}, |
| 133 | {Title: "endoflife.date (community-maintained lifecycle data)", URL: "https://endoflife.date/"}}, |
| 134 | Standards: check.Standards{CWE: []string{"CWE-1104"}, OWASP: []string{"A06:2021"}, NIST: []string{"SA-22", "SI-2"}, CISTopic: "Supported operating system versions"}, |
| 135 | Run: func(_ context.Context, s sys.System) []check.Result { |
| 136 | osr := collect.OSRelease(s) |
| 137 | id, ver := osr["ID"], osr["VERSION_ID"] |
| 138 | if id == "" { |
| 139 | return []check.Result{check.SkipR("os-release", "Distribution could not be identified")} |
| 140 | } |
| 141 | name := osr["PRETTY_NAME"] |
| 142 | if name == "" { |
| 143 | name = id + " " + ver |
| 144 | } |
| 145 | end, bad, known, left := osEOL(id, ver, time.Now()) |
| 146 | switch { |
| 147 | case !known: |
| 148 | return []check.Result{check.SkipR(name, "Release not in the end-of-life table (dated "+EOLTableDate+")")} |
| 149 | case bad && end == "": |
| 150 | return []check.Result{check.FailR(name, "Release is older than every supported release in the end-of-life table (dated "+EOLTableDate+"), so security support has ended")} |
| 151 | case bad && left < 0: |
| 152 | return []check.Result{check.FailR(name, "Security support ended on "+end)} |
| 153 | case bad: |
| 154 | return []check.Result{check.FailR(name, fmt.Sprintf("Security support ends on %s (in %d days, within the %d-day planning window)", end, left, eolWarnDays))} |
| 155 | } |
| 156 | return []check.Result{check.PassR(name, "Supported until "+end)} |
| 157 | }, |
| 158 | }) |
| 159 | |
| 160 | check.Register(&check.Check{ |
| 161 | ID: "HOST-PKG-001", |
| 162 | Title: "Security Updates Waiting to Be Installed", |
| 163 | Domain: check.DomainHost, |
| 164 | Severity: check.High, |
| 165 | CVSS: "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H", |
| 166 | Summary: "Checks the package manager for published security updates that are not installed yet.", |
| 167 | Rationale: "Most real-world compromises use vulnerabilities for which a fix was already available. Pending security updates are known, public weaknesses.", |
| 168 | Detection: "Runs the package manager in simulation mode only (apt-get -s upgrade, or dnf/yum updateinfo --security). Nothing is downloaded or installed. Lines that come from a security repository are counted.", |
| 169 | Fix: "Debian/Ubuntu: apt-get update && apt-get upgrade . RHEL family: dnf upgrade --security . Consider unattended-upgrades or dnf-automatic for security patches.", |
| 170 | Verify: "Re-run openvapt; this check should pass.", |
| 171 | Refs: []check.Ref{ |
| 172 | {Title: "NIST SP 800-40 Rev. 4, Guide to Enterprise Patch Management Planning", URL: "https://csrc.nist.gov/pubs/sp/800/40/r4/final"}, |
| 173 | {Title: "Debian, UnattendedUpgrades", URL: "https://wiki.debian.org/UnattendedUpgrades"}}, |
| 174 | Standards: check.Standards{CWE: []string{"CWE-1104"}, OWASP: []string{"A06:2021"}, NIST: []string{"SI-2", "RA-5"}, CISTopic: "Patch management: install security updates"}, |
| 175 | Run: func(ctx context.Context, s sys.System) []check.Result { |
| 176 | if out, err := s.Run(ctx, "apt-get", "-s", "upgrade"); err == nil { |
| 177 | var sec []string |
| 178 | for _, line := range strings.Split(out, "\n") { |
| 179 | if strings.HasPrefix(line, "Inst ") && strings.Contains(strings.ToLower(line), "security") { |
| 180 | f := strings.Fields(line) |
| 181 | if len(f) > 1 { |
| 182 | sec = append(sec, f[1]) |
| 183 | } |
| 184 | } |
| 185 | } |
| 186 | if len(sec) > 0 { |
| 187 | return []check.Result{check.FailR("apt", fmt.Sprintf("%d security updates pending", len(sec)), firstN(sec, 30)...)} |
| 188 | } |
| 189 | return []check.Result{check.PassR("apt", "No pending security updates (based on the last apt-get update)")} |
| 190 | } |
| 191 | for _, cmd := range [][]string{{"dnf", "-q", "updateinfo", "list", "--security"}, {"yum", "-q", "updateinfo", "list", "security"}} { |
| 192 | if out, err := s.Run(ctx, cmd[0], cmd[1:]...); err == nil { |
| 193 | var sec []string |
| 194 | for _, line := range strings.Split(out, "\n") { |
| 195 | // Only advisory lines count ("ALSA-2024:1234 Important/Sec. pkg"), |
| 196 | // never the header or a footer dnf may print. |
| 197 | if f := strings.Fields(line); len(f) >= 3 && advisoryIDRe.MatchString(f[0]) { |
| 198 | sec = append(sec, f[0]+" "+f[2]) |
| 199 | } |
| 200 | } |
| 201 | if len(sec) > 0 { |
| 202 | return []check.Result{check.FailR(cmd[0], fmt.Sprintf("%d security advisories pending", len(sec)), firstN(sec, 30)...)} |
| 203 | } |
| 204 | return []check.Result{check.PassR(cmd[0], "No pending security advisories")} |
| 205 | } |
| 206 | } |
| 207 | return []check.Result{check.SkipR("packages", "No supported package manager found (apt, dnf, yum)")} |
| 208 | }, |
| 209 | }) |
| 210 | } |
| 211 | |
| 212 | // advisoryIDRe matches vendor advisory ids such as RHSA-2024:1234, |
| 213 | // ALSA-2024:5678, RLSA-2024:1 or FEDORA-2024-abcdef. |
| 214 | var advisoryIDRe = regexp.MustCompile(`^[A-Z]{2,10}-\d{4}[:-][0-9a-zA-Z]+$`) |
| 215 | |
| 216 | func firstN(in []string, n int) []string { |
| 217 | if len(in) <= n { |
| 218 | return in |
| 219 | } |
| 220 | return append(in[:n:n], fmt.Sprintf("... and %d more", len(in)-n)) |
| 221 | } |
Also available as plain Markdown for notes, scripts and AI tools. Same facts, same source, no styling.
Cite This Page
If this page helped your work, please cite it. The version and web address are filled in; the access date is today's date in your browser. Every other style, and when credit is required, is on How to Cite OpenVAPT.
OpenVAPT. (2026). HOST-OS-001 Operating System Past End of Security Support (Version 0.1.0-alpha.6). OpenVAPT Check Catalogue. Retrieved October 16, 2026, from https://openvapt.com/checks/HOST-OS-001.html
In text: (OpenVAPT, 2026). Accessed: the date shown is today's date from your own browser (the site's build date without JavaScript); change it to the day you read the page.
@misc{openvapt_host_os_001,
author = {{OpenVAPT}},
title = {{HOST-OS-001 Operating System Past End of Security Support}},
year = {2026},
howpublished = {\url{https://openvapt.com/checks/HOST-OS-001.html}},
note = {OpenVAPT Check Catalogue, version 0.1.0-alpha.6. Accessed 16 October 2026}
}
Accessed: the date shown is today's date from your own browser (the site's build date without JavaScript); change it to the day you read the page.
Citing OpenVAPT does not mean OpenVAPT endorses, approves or has reviewed your work.