Skip to content

feat: Apus certificate authority - #460

Draft
raphael-goetz wants to merge 6 commits into
mainfrom
feat/init-apus
Draft

raphael-goetz wants to merge 6 commits into
mainfrom
feat/init-apus

Conversation

@raphael-goetz

Copy link
Copy Markdown
Member

Summary

  • Adds a real certificate authority (src/ca) using rcgen, replacing the bench/tls.sh hack in the apus POC: issues short-lived action client certs (URI SAN code0://action/<id>) and Taurus server certs
  • Adds a hand-rolled local gRPC CertificateService exposing issuance, gated on JWT auth (has_action/has_runtime), wired into both dynamic and static server startup
  • apus-protocol was evaluated but not added as a dependency — it has nothing CA-side reusable (issuance crate rcgen is only a dev-dependency there); identity_uri was reimplemented locally to match byte-for-byte
  • Includes an unrelated pre-existing fix: origin/main didn't actually build (result_trim.rs wasn't updated for tucana 0.0.83's NodeValueExecutionResult wrapper) — fixed in its own commit, happy to split into a separate PR if preferred

Known gaps (flagged, not fixed here)

  • projects on an action cert request isn't validated against what the action is actually authorized for
  • No CA key rotation/custody story (file path only)
  • The new gRPC service currently has zero consumers (no client in apus-client/taurus calls it yet)

Test plan

  • cargo build
  • cargo test — 89/89 (61 pre-existing + 28 new)
  • cargo clippy --all-targets — no new warnings
  • cargo fmt --check — clean except one pre-existing unrelated file

🤖 Generated with Claude Code

…wrapper

Unrelated pre-existing breakage found while starting the Apus CA work:
origin/main did not compile (nor did its tests) because the tucana
0.0.82 -> 0.0.83 bump (20ec661) wrapped node success values in a new
NodeValueExecutionResult { value: oneof { RawValue, SubFlowFunctionResult } }
message, but this file still built/matched the old NodeParameterNodeExecutionResult
shape directly. Fixed to compile so the branch has a working baseline
to build the certificate authority on top of.
rcgen issues the certificates the certificate authority hands out
(action and server leaf certs plus the ephemeral/loaded CA); time
backs its expiry arithmetic. x509-parser and rustls are dev-only, to
parse issued certificates back and verify they chain to the CA in
tests.
Aquila's side of Apus mutual TLS: CertificateAuthority issues short-lived
action client certificates (URI SAN code0://action/<id>[?projects=][&replicas=],
the identity Taurus checks HELLO.action against) and Taurus server
certificates, backed by rcgen. identity_uri/is_valid_action_identifier in
src/ca/identity.rs are a deliberate, documented copy of
apus-protocol::tls's identity_uri/parse_identity (not a runtime dependency
on the sibling apus repo - see the rationale in identity.rs's module doc).

load() generates an ephemeral, loudly-logged self-signed CA when no CA
path is configured in development, and hard-errors in staging/production
instead of ever running an un-provisioned CA outside development.

Also: gofmt cleanup of the result_trim.rs fix from the previous commit
(dropped a now-unused import, matched this repo's formatting).
Adds config.apus: enabled, ca_cert_path/ca_key_path (both or neither),
action/server certificate lifetimes (default 24h / 90d), and output_dir
for the interim file bridge. Rendered in the startup config display -
only paths, no key material is ever held in this config value so there's
nothing to filter.
Adds a local, hand-written aquila.apus.CertificateService (no .proto, no
tonic-build step - the message types and the tonic server dispatcher are
written by hand to the same shape codegen would produce, mirroring
tucana's generated module_service_server for the boilerplate). Its one
RPC, IssueCertificate, authenticates the caller's existing JWT the same
way every other Aquila RPC does, then:

- Action subject: requires has_action(token, identifier), issues a
  client certificate via CertificateAuthority::issue_action_certificate
  (replicas comes from the service configuration, not the request -
  projects is taken from the request as-is, flagged as a follow-up since
  there's no existing authoritative source to validate it against).
- Server subject: requires has_runtime(token, identifier) (Taurus
  authenticates as "taurus"), issues a server certificate.

Authorization is two-phase so failures map to the right status: no
has_service match at all -> Unauthenticated (wrong secret, or the
identifier isn't provisioned); a real identity requesting the wrong
subject kind -> PermissionDenied.

Adds tonic-prost as a direct dependency (for ProstCodec, used by the
hand-written server dispatcher).
Loads CertificateAuthority once at startup in both startup::dynamic_mode
and startup::static_mode, gated on config.apus.enabled (panics with a
clear message on load failure, matching this module's existing fail-fast
style for other startup dependencies). Threads it through as
Option<Arc<CertificateAuthority>> into AquilaDynamicServer/AquilaStaticServer,
which register the CertificateService via tonic's add_optional_service
(not behind the Sagittarius readiness interceptor - issuing a certificate
has no Sagittarius dependency) in every Server::builder() branch, health
and non-health, both modes.

Also introduces StaticServerDependencies, mirroring dynamic_server's
existing DynamicServerDependencies pattern, since AquilaStaticServer::new
had grown past clippy::too_many_arguments once apus_ca was added.
@github-actions

github-actions Bot commented Nov 1, 2026

Copy link
Copy Markdown

GitLab Pipeline Action

General information

Link to pipeline: https://gitlab.com/code0-tech/development/aquila/-/pipelines/3014405849

Status: Passed
Duration: 1 minutes

Job summaries

docs:preview

Documentation preview available at https://code0-tech.gitlab.io/-/development/telescopium/-/jobs/17643443570/artifacts/out/index.html

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