Skip to content

Release 0.5.0 - #192

Merged
Gorka Gonzalo (d0ubl3g) merged 44 commits into
mainfrom
dev
Oct 15, 2026
Merged

Gorka Gonzalo (d0ubl3g) merged 44 commits into
mainfrom
dev

Conversation

@d0ubl3g

@d0ubl3g Gorka Gonzalo (d0ubl3g) commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Release 0.5.0: merges dev into main. The full notes are the ## [0.5.0] section of CHANGELOG.md, which also becomes the release notes.

Added

Changed

Fixed

Tests

No change to the scripting surface that breaks a script: the new --json fields are additions. macOS binaries remain without a Developer ID signature in this release, as in 0.4.4.

Replace the proposal with the decision: keep the keychain as the default
and add an explicit opt-in token store that is a plain-text file, with no
fallback in either direction. Record the file checks, the location in the
state directory, the deferral of Windows and of the Linux kernel keyring,
and the server-side refresh token lifetime that is still to be confirmed.
The text now matches the fail-closed sign-in behaviour, where tokens that
cannot be stored are revoked.

Refs #39

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
People who work from a jump host or another machine without a keychain
could only use a tenant API key. Add a second token store, selected by
HODEISHIELD_TOKEN_STORE=plaintext-file or the global token_store setting,
that keeps one file per profile in the state directory.

The keychain stays the default and the two stores never fall back to each
other. Every read and write refuses, without repairing, a symbolic link,
a file or directory owned by another user, or one open to group or
others. Files are created with mode 600 and replaced atomically. The
effective user id comes from rustix, which is already in the dependency
graph on Linux, so no unsafe code is needed. Windows is refused for now.

Refs #39

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
Add a README section for servers, containers and remote sessions that
puts API keys first and the opt-in file second with its trade-off, the
new variable and setting in their tables, the network home directory
note, and the exit code for a refused token file. Update the places that
promised tokens are never in a file, and add the changelog entry.

Refs #39

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
Say that on macOS the sessions directory is a sibling of the application
support folder, not inside the configuration folder, that the parents of
the sessions directory must not be writable by other users, that the
file names are a hash and not a way to hide a profile name, that running
as root for a user's home does not work, and that a refused file makes
login and logout exit 1 while other commands exit 3.

Refs #39

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
Keep the sessions outside the configuration directory on macOS, where the
platform gives the same folder for both. Open the sessions directory
once without following links, check it on the open handle and do every
later operation relative to it, so replacing the path after the check
cannot redirect a read, write or removal. Open files without following
links and without blocking, remove stale temporary files that a crash
could leave with live tokens, and choose the store once per process so a
command checks and saves in the same one. State precisely in the message
that group and others must have no access. Tests cover the temporary file
guard, the leftovers, the opposite direction of the missing fallback,
and that a refused file is left byte for byte as it was.

Refs #39

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
The sessions moved to a sibling of the configuration directory on macOS; the integration tests still looked inside it, so on macOS they found no file.

Refs #39.

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
The service's token lifetimes are now known: access tokens last 15 minutes, refresh tokens 30 days from the last refresh with reuse detection, and an absolute 90-day limit is being added. The README and the decision record state today's exposure of a leaked file and how to end it, instead of an open question.

Refs #39.

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
The test moved the sessions directory into the state directory, which does not exist on macOS (the sessions directory sits under Application Support there), so the move failed before the case under test ran.

Refs #39.

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
Its only users are the Linux check and a Unix-only test, so on a Windows test build it was dead code and clippy refused it.

Refs #39.

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>

# Conflicts:
#	crates/hodeishield-cli/src/cli.rs
#	crates/hodeishield-cli/src/commands/mod.rs
#	crates/hodeishield-cli/tests/cli.rs
The service now caps a refresh chain at 90 days from the original
sign-in and ends CLI sessions on a password change or reset, an
administrator revocation or removal from the tenant. The README and
ADR 0001 give these figures instead of the earlier "is adding" wording.
The changelog entry moves from the released 0.4.0 section, where the
merge of dev left it, to Unreleased.

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
CodeQL (rust/cleartext-logging) reads the assert_eq! on the effective
uid as writing it to a log. A uid is not secret, but the comparison
needs no values in its message either.

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
CodeQL (rust/cleartext-logging) flags the assertion that prints the
refusal reason, which names the file owner's uid. The assertion now
prints nothing; the effective-uid comparison goes back to assert_eq!,
which was not the flagged line.

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
The vendored document adds GET /v1/me, the reports operations and the
known values of the risks domain and status and the compliance controls
status filters. The generator now accepts an operation that needs no
scope (x-hs-scope none) and an answer that is a file: such an operation
returns a Download with the bytes, the content type and the file name,
read up to the document's 20 MiB limit, and sends an Accept header with
the declared media types. Two shapes that share a name are renamed
(WhoAmIOrganization, ReportDetail).

The filter tests now cover the warning for each newly listed filter;
the vendors category filter stays free text. Sign-in asks for
reports:read when the app offers it.

Closes #145

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
Homebrew now refuses to load a formula from a tap that is not trusted,
and adding a tap does not trust it, so the documented brew tap then
brew install hodeishield fails. Installing the fully qualified name adds
the tap and trusts only this formula. The README says so and how to
trust the formula when the tap was added earlier.

Refs #179

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
Add an opt-in plain-text token store for machines without a keychain

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>

# Conflicts:
#	CHANGELOG.md
whoami reads GET /v1/me and shows the tenant the credential reads (name
and id), its organisation and, for a key, its scopes; for a portfolio
key, the MSSP and the number of client tenants. --json gains the
tenant, organization, portfolio and credential_scopes fields. When the
API cannot be read, whoami still prints the local lines, the new fields
are null and a warning says why; the exit code does not change. Names
from the API are cleaned before they are printed.

Closes #48

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
The reason a GET /v1/me call failed can carry the API's own message,
which is untrusted; the warning printed it as received. It now goes
through clean(), like every other API string the CLI prints, and the
contract test checks that no escape sequence reaches the terminal.

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
Install with Homebrew by the formula's full name

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>

# Conflicts:
#	CHANGELOG.md
Follow version 2026-10-19 of the API document

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
reports list filters by --type (also --report-type), --status and
--since, with --sort, paging, --json, --csv, --tenant and --all-tenants
like the other lists; the listed type and status values are shown in
--help and an unlisted one gets a warning. reports get shows a report
with the formats it can be downloaded in.

reports download saves a report's file (--format pdf|xlsx, --locale) in
the current directory under the name the API suggests, reduced to a
plain file name (last component only, [A-Za-z0-9._-], no leading dots
or dashes, no Windows device names), or report-<id>.<format> when
nothing usable is left. --output saves it at a path or into a
directory; --output - writes it to standard output and is refused when
that is a terminal. The file goes to a temporary file in the same
directory and is renamed into place; without --force the destination
is first claimed with an exclusive create, so an existing file is never
replaced. With --json, standard output holds the path, size and content
type.

The 409 answers of the download (REPORT_NOT_READY, FORMAT_UNSUPPORTED,
FILE_TOO_LARGE) get their own messages and hints, still with exit code
1. All server text printed on either stream is cleaned.

Closes #55

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>

# Conflicts:
#	CHANGELOG.md
Show the credential's tenant in whoami

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
The extension of a saved report is always that of its format, whatever name the API suggests. The file is written and synced under a temporary name and then linked into place, so an interrupted save leaves nothing behind, and on Unix it is readable only by its owner. The target directory is checked before the download, and error bodies are read with the normal size limit.

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
Add the reports commands: list, get and download

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
The README now describes the System Settings route (Privacy & Security, Open
Anyway), says that unpacking with tar in Terminal keeps the quarantine flag
while a curl download carries none, and notes that the Gatekeeper dialog
follows the system language.

Refs #16

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
Add .well-known/security.txt (RFC 9116) with the private reporting link,
the security address, the preferred languages, the policy and the
canonical URL. SECURITY.md, the risk assessment and the technical
documentation point to it.

A weekly workflow (and any pull request that changes the file or the
check) validates the file and fails when it is invalid, has expired or
has less than 60 days left; the scheduled run opens an issue labelled
security-txt and closes it when the check passes again.

Closes #125

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
Explain how to open a blocked macOS binary

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
a_connection_error_is_retried bound a listener on an ephemeral port,
dropped it and expected every connection to that port to be refused.
Between the drop and the client's attempts, another test running in
parallel (mockito servers bind ephemeral ports) could receive the same
port, so the client got an answer and the retry count was not 2.

The test now keeps the listener bound for its whole duration, so the
port cannot be reused. The listener never accepts, so each attempt
times out (short client timeout), which the client retries the same way
as a refused connection. A reset after accept would not be retried, as
it is not a connect error, so accepting and closing was not used. The
assertions are unchanged: Error::Transport and exactly 2 retries.

Closes #183

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
Publish a security.txt and check its expiry

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
Keep the refused-connection test from racing other tests

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
The release workflow already holds the macOS signing steps behind the
MACOS_SIGNING_ENABLED variable. This makes them safe to switch on:

- When signing is off, the macOS job says so in the run summary and as
  a notice, so a skip cannot be read as a pass.
- When signing is on, the job checks the Developer ID Application
  authority, the Team ID, the hardened runtime and the timestamp after
  signing, and asserts that Gatekeeper reports the binary as Notarized
  Developer ID and that the package is signed, stapled and accepted. It
  writes the result to the run summary.
- A new on-demand "macOS signing check" workflow, bound to the
  release-signing environment, runs the same chain on a throwaway
  executable and package. It fails and lists the names of any missing
  secret.
- docs/release-signing.md describes the identities, the secrets, who may
  change them and what to do if they are compromised;
  docs/verifying-releases.md explains how to verify the signatures.

The signing steps stay inline in both workflows: the signing job checks
nothing out and runs no repository code, and a shared script would
break that.

Refs #50

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
whoami now reads the user of a sign-in from the body of GET /v1/me as
received (data.user), not from the generated type, because the vendored
API document does not describe the field yet. It is read leniently: a
missing field, null or a malformed value is no user, never an error.

The User line and the --json user string keep their name and type. They
are filled from the API first, formatted as Name <email>, and the
authorization server's userinfo endpoint is only asked when the API
gives no user. For a key the field is ignored. Every string is cleaned
before it is printed.

No extra request is made, and nothing changes in the output until the
API returns the field.

Closes #182

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
Show the signed-in user in whoami from /v1/me

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
Prepare Developer ID signing and notarization for macOS

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
The default procedure to enable macOS signing is to store the nine secrets and set MACOS_SIGNING_ENABLED to true: the next release signs and notarizes, and its draft is published only after the macOS job and its checks pass. The signing check workflow stays documented as an optional way to check the secrets before a release, with its two preconditions.

Refs #50

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
Make the macOS signing check optional in the release-signing steps

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
Version 0.5.0 (workspace crates only in Cargo.lock), the changelog
section dated 2026-10-15 with an empty Unreleased section above it and
the compare links, the version in the examples of the README and
docs/verifying-releases.md, and 0.5.0 in the range of the Authenticode
table.

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
Prepare 0.5.0: version and changelog

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
@d0ubl3g
Gorka Gonzalo (d0ubl3g) merged commit 9950494 into main Oct 15, 2026
42 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant