Conversation
Cache miss refill covers new secrets, and psi cache refresh covers
manual rotation, but a secret rotated upstream between reboots stayed
stale until an operator intervened or the host restarted. Add a
systemd timer generated alongside the provider setup unit that
re-runs psi-{provider}-setup.service on a configurable cadence so
rotations propagate without manual action.
CacheConfig gets refresh_interval (default 1h) and
refresh_randomized_delay (default 5m) — systemd time strings that
map directly to OnUnitActiveSec and RandomizedDelaySec on the
generated timer. The timer uses OnBootSec + OnUnitActiveSec +
Persistent=true so the schedule resumes correctly after downtime.
The timer is only emitted when the cache is enabled with a backend
AND the provider is in the refreshable set. Today that is just
infisical; nitrokeyhsm is local-only and has nothing to re-fetch.
The same helper runs for native and container-mode installs since
timers are plain systemd units regardless of whether the service
they trigger comes from a quadlet or a handwritten .service file.
Operators can override the cadence per host without editing config
via systemctl edit psi-infisical-setup.timer — documented in the
rotation section of docs/secret-cache.md.
jdoss
added a commit
that referenced
this pull request
Apr 8, 2026
PR #20 generated psi-{provider}-setup.timer pointing directly at the existing setup unit, but that never fired more than once. The setup service uses Type=oneshot + RemainAfterExit=yes so ActiveEnterTimestamp is set once and never updates, and OnUnitActiveSec on the timer is anchored to that frozen timestamp. Once the first fire happens, 'next fire' = ActiveEnterTimestamp + interval — already in the past, so systemd sets next_elapse to infinity and the timer never re-arms. Worse, even if it did fire again, systemctl start on a oneshot that is currently active (exited) is a no-op, so the cache would not update. Fix: generate a tiny wrapper psi-{provider}-refresh.service (plain oneshot, no RemainAfterExit) that does: ExecStart=/usr/bin/systemctl restart psi-{provider}-setup.service Point the timer at the wrapper. The wrapper's ActiveEnterTimestamp moves forward every run, OnUnitActiveSec re-arms correctly, and systemctl restart on the setup unit does re-run its ExecStart and repopulate the cache. Verified end-to-end on a test host with a 2 minute interval: the second and third scheduled runs both rewrote cache.enc and the timer showed NEXT/LEFT for the next cycle each time.
6 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
psi-{provider}-setup.timergenerated alongside the setup service, triggers the same unit oncache.refresh_interval(default1h). Today onlypsi-infisical-setup.timeris emitted — nitrokeyhsm is local-only.CacheConfigfields:refresh_intervalandrefresh_randomized_delay. Both accept systemd time strings.psi systemd installwrites and enables the timer automatically when the cache is configured. Works for both native and container modes.docs/secret-cache.md,README.md, andCLAUDE.md.Why
Before this PR, PSI populated the cache at boot and on lookup miss, and operators could run
psi cache refreshmanually. A secret rotated in Infisical between reboots stayed stale until someone intervened or the host restarted. Option (a) from the cache-sync design discussion was the lowest-effort fix: an external systemd timer that kicks the existing setup unit. This matches the existing pattern forpsi-tls-renew.timerand keeps serve code untouched.What changes
psi/settings.pyCacheConfig.refresh_interval: str = "1h"CacheConfig.refresh_randomized_delay: str = "5m"Both are passed straight through to systemd, so any valid systemd time string works (
30m,2h,1d, etc.).psi/unitgen.pygenerate_provider_setup_timer(provider, interval, randomized_delay)— builds a.timerunit targetingpsi-{provider}-setup.service. UsesOnBootSec+OnUnitActiveSec+Persistent=trueso the schedule survives reboots and missed intervals.provider_supports_refresh(provider)— returnsTrueonly for providers whose setup path talks to a remote. Set of one today:{"infisical"}.psi/installer.py_write_refresh_timershelper emits the timer for each refreshable provider. Returns early ifcache.enabledisFalseorcache.backendisNone— no point scheduling refreshes if PSI is not caching._install_nativeand_install_containerboth call it. Timers live under the systemd unit dir in both modes (timers are plain units regardless of whether the triggered service comes from a quadlet)._enable_unitsgains arefresh_timerskwarg and enables each timer withsystemctl enable --nowwhen--enableis passed.Tests
tests/test_unitgen.py:TestProviderRefreshSupport(3 tests) andTestProviderSetupTimer(5 tests) covering the target unit, interval/delay pass-through,Persistent=true, the[Install]section, and the description text.tests/test_installer.py:TestWriteRefreshTimers(5 tests) covering the happy path, cache disabled, no backend configured, nitrokeyhsm-only providers, and custom interval.Docs
docs/secret-cache.md: rewrites the rotation section into "Rotation and periodic refresh" with three subsections — scheduled refresh (timer details, tuning viasystemctl edit), manual refresh (CLI commands), and on-miss refill (what the serve process already does). Addsrefresh_intervalandrefresh_randomized_delayto the configuration example.README.md: updates the cache config example to include the new fields, adds a paragraph about the auto-generated timer, addspsi-infisical-setup.timerto the FCOS generator list, and notes in the CLI reference thatpsi cache refreshis only needed for out-of-band rotations.CLAUDE.md: same CLI-reference note.Test plan
uv run ruff check psi/ tests/— cleanuv run ruff format --check psi/ tests/— cleanuv run ty check— cleanuv run pytest -q— 309 passed (13 new tests)psi-infisical-setup.timerexists andsystemctl list-timersshows the next run atrefresh_intervalfrom bootpodman exec psi-secrets psi cache status --verifyand an actual container lookup