A Time-Boxed Security Exception: Kubernetes apt Signatures on Debian 13

On 1 February 2026, Debian 13's package verifier started rejecting the signatures on the official Kubernetes apt repository. My node bootstrap script stopped working. Getting the install through took a short, scoped exception to a security control. This post covers the failure, the exception, and how it should be managed: what it exposes, what limits it, and when it ends.

Updated October 2026. The first version of this post (February 2026) described the workaround as a "bypass" and stopped at the technical fix. This version adds the exception record, the gaps I found in the original script, and the exit plan.

Impact

  • The automated Kubernetes install on Debian 13 (Trixie) stopped at apt-get update: no kubeadm, kubelet or kubectl could be installed.
  • Every new node bootstrapped with the script was blocked until a decision was made.
  • No running node was affected: the failure only hit new installs and package updates.

Timeline

  1. The script added the official Kubernetes pkgs.k8s.io repository and ran apt-get update.
  2. apt failed hard:
Sub-process /usr/bin/sqv returned an error code (1)
Error: Policy rejected packet type
Caused by: Signature Packet v3 is not considered secure since 2026-02-01T00:00:00Z
  1. I tried an override in /etc/crypto-policies/back-ends/apt-sequoia.config. It didn't take effect in my setup. It would also have relaxed the policy permanently and for every repository, which is wider than the problem.
  2. I tried passing Apt::Key::sqvOptions in apt.conf, but the directive wasn't supported.
  3. I found that sqv accepts --policy-as-of, which evaluates a signature against the policy in force at an earlier date. That made a narrow, temporary exception possible.
  4. I wrapped sqv for the duration of the install script only (see Actions), and the install went through.

Root cause

Debian 13 verifies repository signatures with Sequoia (sqv). Its default policy stops accepting OpenPGP v3 signature packets from 1 February 2026.

The pkgs.k8s.io repositories are still signed with v3 signatures. They are generated on the openSUSE Build Service, and re-signing depends on that platform. The problem is tracked upstream in kubernetes/release#3869.

So this was not a misconfiguration on my side. A distribution enforced a stricter control before an upstream supplier had caught up. That situation is common, and it is exactly what an exception process is for.

The exception record

This is the record the change should have had in February, written retrospectively.

Field Value
Control OS package signature policy: reject OpenPGP v3 signatures (Debian 13 / Sequoia default).
Deviation During the node install script only, signatures are evaluated against the policy of 1 Jan 2025, which still accepts v3.
Scope Nodes being bootstrapped, for the duration of one script run. The deviation applies to every repository apt verifies during that window, not only Kubernetes.
Risk v3 signatures are deprecated because of weaknesses in the older format. During the window, a forged or downgraded signature on any configured repository would be accepted.
Likelihood Low: a window of minutes, on a home network, against official repositories only.
Compensating controls Kubernetes packages held at the installed version (apt-mark hold); the wrapper removed automatically when the script ends; a check after every run that the real sqv binary is back (see Actions).
Owner / approver Me. In a single-operator lab there is no separation of duties; in a company this would need sign-off from the system owner and the security team.
Review At every Kubernetes minor-version upgrade.
Exit criterion The pkgs.k8s.io repositories are signed with v4 signatures, or the install moves to checksum-verified binaries (see Exit plan).

Actions

The script swaps sqv for a wrapper that adds --policy-as-of, and puts the real binary back when the script exits.

Reviewing the original version, I found two gaps:

  • The trap was registered after the binary was moved. If writing the wrapper failed, the script exited with sqv missing and nothing to restore it, which breaks apt for every package on the node.
  • trap ... EXIT does not run on SIGKILL, an OOM kill or a power loss. In those cases the wrapper stays in place silently, and the exception becomes permanent without anyone deciding it.

The corrected version:

set -euo pipefail

# 1. Register the restore step first, so it runs even if the next steps fail
restore_sqv() {
  if [ -f /usr/bin/sqv.real ]; then
    sudo mv -f /usr/bin/sqv.real /usr/bin/sqv
  fi
}
trap restore_sqv EXIT

# 2. Move the real binary aside and install the wrapper
sudo mv /usr/bin/sqv /usr/bin/sqv.real
sudo tee /usr/bin/sqv > /dev/null <<'WRAPPER'
#!/usr/bin/env bash
# Temporary exception: evaluate signatures against the 2025 policy (accepts v3)
exec /usr/bin/sqv.real --policy-as-of 2025-01-01T00:00:00Z "$@"
WRAPPER
sudo chmod 755 /usr/bin/sqv

# 3. Install and pin the exact versions
sudo apt-get update
sudo apt-get install -y kubelet kubeadm kubectl
sudo apt-mark hold kubelet kubeadm kubectl

The trap covers the normal cases. For the others, the restore has to be checked, not assumed. The fix is a step in the provisioning playbook, run after every install, that checks the installed sqv matches its Debian package:

# Prints nothing when the files of the `sqv` package are unmodified
dpkg --verify sqv

Any output fails the playbook. That detective control is what keeps a "temporary" exception from quietly becoming permanent.

Exit plan

  1. Preferred: upstream re-signs the repositories with v4 signatures. Then I remove the wrapper block from the script and close the exception. I check the upstream issue at each review.
  2. If upstream stalls: install kubeadm, kubelet and kubectl as release binaries from dl.k8s.io, verified against the published SHA-256 checksums. That needs no exception at all, at the cost of managing the kubelet service unit myself and losing apt upgrades.

Lessons learned

  • A blocked control is a decision, not an obstacle. The question is not "how do I get past this?" but "do I accept this risk, for how long, and with what limits?" Writing that down takes ten minutes and makes the trade-off visible.
  • Scope the exception as narrowly as the tools allow, and say how wide it really is. My wrapper looked like a Kubernetes-only fix but relaxed the policy for every repository during the window. The record should say so.
  • "Temporary" needs an expiry and a check. A cleanup step that runs in the happy path is not enough: something has to verify that the control is back.
  • Supplier lag is a normal risk. Upstream projects don't always move in step with the platforms they ship to. Expect it, and track the upstream issue as part of the exception.

References

Links checked October 2026.

Sources

Further reading