detached 72f8f2e

feat(ci): OIDC ID tokens for CI jobs (#14)

commit
72f8f2e940f120e52aae70f75492365ad1c30225
author
brice · · 2026-10-16 07:40 UTC
committer
bnema · · 2026-10-16 07:40 UTC
parent
faa62b9
signature
SSH UnverifiedSHA256:VOCA5lYARHcbKDZ7GhTV45VmcjWnJ6ZQIBazj17LivA The key is unknown
refs
tag: v0.4.0
7 files · +298 -20
M README.md +1 -1
@@ -29,7 +29,7 @@ Handlers are mounted under `/-/api/` by the server. Errors are Connect codes.
2929 | `EnvironmentService` | `CreateEnvironment`, `ListEnvironments`, `DeleteEnvironment` |
3030 | `JobArtifactService` | `CreateJobArtifact`, `UploadJobArtifactBlock` (client stream), `CommitJobArtifact`, `FinalizeJobArtifact`, `ListJobArtifacts`, `DownloadJobArtifact` (streamed, resumable), `DeleteJobArtifact`; artifacts v4 for CI runners, scoped to the run of the job token |
3131 | `ArtifactService` | `ListArtifacts`, `DownloadArtifact` (streamed, resumable), `DeleteArtifact`; artifacts of a run for people |
32-| `ServerService` | `GetVersion` (server version and feature list), `GetSigningKey` (commit signing key) |
32+| `ServerService` | `GetVersion` (server version and feature list), `GetSigningKey` (commit signing key), `RotateOIDCKey` (key of the CI OIDC ID tokens, owner only) |
3333
3434 Tokens carry scopes only; presets such as `cli` or `agent` are a client concern.
3535
@@ -29,7 +29,7 @@ Handlers are mounted under `/-/api/` by the server. Errors are Connect codes.
29 | `EnvironmentService` | `CreateEnvironment`, `ListEnvironments`, `DeleteEnvironment` |29 | `EnvironmentService` | `CreateEnvironment`, `ListEnvironments`, `DeleteEnvironment` |
30 | `JobArtifactService` | `CreateJobArtifact`, `UploadJobArtifactBlock` (client stream), `CommitJobArtifact`, `FinalizeJobArtifact`, `ListJobArtifacts`, `DownloadJobArtifact` (streamed, resumable), `DeleteJobArtifact`; artifacts v4 for CI runners, scoped to the run of the job token |30 | `JobArtifactService` | `CreateJobArtifact`, `UploadJobArtifactBlock` (client stream), `CommitJobArtifact`, `FinalizeJobArtifact`, `ListJobArtifacts`, `DownloadJobArtifact` (streamed, resumable), `DeleteJobArtifact`; artifacts v4 for CI runners, scoped to the run of the job token |
31 | `ArtifactService` | `ListArtifacts`, `DownloadArtifact` (streamed, resumable), `DeleteArtifact`; artifacts of a run for people |31 | `ArtifactService` | `ListArtifacts`, `DownloadArtifact` (streamed, resumable), `DeleteArtifact`; artifacts of a run for people |
32-| `ServerService` | `GetVersion` (server version and feature list), `GetSigningKey` (commit signing key) |32+| `ServerService` | `GetVersion` (server version and feature list), `GetSigningKey` (commit signing key), `RotateOIDCKey` (key of the CI OIDC ID tokens, owner only) |
33 33
34 Tokens carry scopes only; presets such as `cli` or `agent` are a client concern.34 Tokens carry scopes only; presets such as `cli` or `agent` are a client concern.
35 35
M gen/ermite/ermiteconnect/server.connect.go +49 -0
@@ -41,6 +41,9 @@ const (
4141 // ServerServiceGetSigningKeyProcedure is the fully-qualified name of the ServerService's
4242 // GetSigningKey RPC.
4343 ServerServiceGetSigningKeyProcedure = "/ermite.ServerService/GetSigningKey"
44+ // ServerServiceRotateOIDCKeyProcedure is the fully-qualified name of the ServerService's
45+ // RotateOIDCKey RPC.
46+ ServerServiceRotateOIDCKeyProcedure = "/ermite.ServerService/RotateOIDCKey"
4447 )
4548
4649 // ServerServiceClient is a client for the ermite.ServerService service.
@@ -55,6 +58,17 @@ type ServerServiceClient interface {
5558 // Servers that implement it list "signing_key" in
5659 // GetVersionResponse.features.
5760 GetSigningKey(context.Context, *connect.Request[ermite.GetSigningKeyRequest]) (*connect.Response[ermite.GetSigningKeyResponse], error)
61+ // RotateOIDCKey replaces the key with which the server signs the OIDC ID
62+ // tokens of CI jobs (Job.id_token_url) by a new one. The previous key stops
63+ // signing at once but stays in the server's JWKS
64+ // ("/-/oidc/.well-known/jwks") for 24 hours, so that tokens it signed still
65+ // verify until they expire. It needs owner rights: the admin token, the
66+ // owner's session, or a token with every scope and no repository list or
67+ // branch prefixes (permission_denied otherwise). It fails with
68+ // failed_precondition when the server issues no ID tokens (it has no
69+ // secrets key to seal the key with). Servers that implement it list
70+ // "ci.oidc" in GetVersionResponse.features.
71+ RotateOIDCKey(context.Context, *connect.Request[ermite.RotateOIDCKeyRequest]) (*connect.Response[ermite.RotateOIDCKeyResponse], error)
5872 }
5973
6074 // NewServerServiceClient constructs a client for the ermite.ServerService service. By default, it
@@ -80,6 +94,12 @@ func NewServerServiceClient(httpClient connect.HTTPClient, baseURL string, opts
8094 connect.WithSchema(serverServiceMethods.ByName("GetSigningKey")),
8195 connect.WithClientOptions(opts...),
8296 ),
97+ rotateOIDCKey: connect.NewClient[ermite.RotateOIDCKeyRequest, ermite.RotateOIDCKeyResponse](
98+ httpClient,
99+ baseURL+ServerServiceRotateOIDCKeyProcedure,
100+ connect.WithSchema(serverServiceMethods.ByName("RotateOIDCKey")),
101+ connect.WithClientOptions(opts...),
102+ ),
83103 }
84104 }
85105
@@ -87,6 +107,7 @@ func NewServerServiceClient(httpClient connect.HTTPClient, baseURL string, opts
87107 type serverServiceClient struct {
88108 getVersion *connect.Client[ermite.GetVersionRequest, ermite.GetVersionResponse]
89109 getSigningKey *connect.Client[ermite.GetSigningKeyRequest, ermite.GetSigningKeyResponse]
110+ rotateOIDCKey *connect.Client[ermite.RotateOIDCKeyRequest, ermite.RotateOIDCKeyResponse]
90111 }
91112
92113 // GetVersion calls ermite.ServerService.GetVersion.
@@ -99,6 +120,11 @@ func (c *serverServiceClient) GetSigningKey(ctx context.Context, req *connect.Re
99120 return c.getSigningKey.CallUnary(ctx, req)
100121 }
101122
123+// RotateOIDCKey calls ermite.ServerService.RotateOIDCKey.
124+func (c *serverServiceClient) RotateOIDCKey(ctx context.Context, req *connect.Request[ermite.RotateOIDCKeyRequest]) (*connect.Response[ermite.RotateOIDCKeyResponse], error) {
125+ return c.rotateOIDCKey.CallUnary(ctx, req)
126+}
127+
102128 // ServerServiceHandler is an implementation of the ermite.ServerService service.
103129 type ServerServiceHandler interface {
104130 // GetVersion returns the server version and the optional capabilities of the
@@ -111,6 +137,17 @@ type ServerServiceHandler interface {
111137 // Servers that implement it list "signing_key" in
112138 // GetVersionResponse.features.
113139 GetSigningKey(context.Context, *connect.Request[ermite.GetSigningKeyRequest]) (*connect.Response[ermite.GetSigningKeyResponse], error)
140+ // RotateOIDCKey replaces the key with which the server signs the OIDC ID
141+ // tokens of CI jobs (Job.id_token_url) by a new one. The previous key stops
142+ // signing at once but stays in the server's JWKS
143+ // ("/-/oidc/.well-known/jwks") for 24 hours, so that tokens it signed still
144+ // verify until they expire. It needs owner rights: the admin token, the
145+ // owner's session, or a token with every scope and no repository list or
146+ // branch prefixes (permission_denied otherwise). It fails with
147+ // failed_precondition when the server issues no ID tokens (it has no
148+ // secrets key to seal the key with). Servers that implement it list
149+ // "ci.oidc" in GetVersionResponse.features.
150+ RotateOIDCKey(context.Context, *connect.Request[ermite.RotateOIDCKeyRequest]) (*connect.Response[ermite.RotateOIDCKeyResponse], error)
114151 }
115152
116153 // NewServerServiceHandler builds an HTTP handler from the service implementation. It returns the
@@ -132,12 +169,20 @@ func NewServerServiceHandler(svc ServerServiceHandler, opts ...connect.HandlerOp
132169 connect.WithSchema(serverServiceMethods.ByName("GetSigningKey")),
133170 connect.WithHandlerOptions(opts...),
134171 )
172+ serverServiceRotateOIDCKeyHandler := connect.NewUnaryHandler(
173+ ServerServiceRotateOIDCKeyProcedure,
174+ svc.RotateOIDCKey,
175+ connect.WithSchema(serverServiceMethods.ByName("RotateOIDCKey")),
176+ connect.WithHandlerOptions(opts...),
177+ )
135178 return "/ermite.ServerService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
136179 switch r.URL.Path {
137180 case ServerServiceGetVersionProcedure:
138181 serverServiceGetVersionHandler.ServeHTTP(w, r)
139182 case ServerServiceGetSigningKeyProcedure:
140183 serverServiceGetSigningKeyHandler.ServeHTTP(w, r)
184+ case ServerServiceRotateOIDCKeyProcedure:
185+ serverServiceRotateOIDCKeyHandler.ServeHTTP(w, r)
141186 default:
142187 http.NotFound(w, r)
143188 }
@@ -154,3 +199,7 @@ func (UnimplementedServerServiceHandler) GetVersion(context.Context, *connect.Re
154199 func (UnimplementedServerServiceHandler) GetSigningKey(context.Context, *connect.Request[ermite.GetSigningKeyRequest]) (*connect.Response[ermite.GetSigningKeyResponse], error) {
155200 return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.ServerService.GetSigningKey is not implemented"))
156201 }
202+
203+func (UnimplementedServerServiceHandler) RotateOIDCKey(context.Context, *connect.Request[ermite.RotateOIDCKeyRequest]) (*connect.Response[ermite.RotateOIDCKeyResponse], error) {
204+ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.ServerService.RotateOIDCKey is not implemented"))
205+}
@@ -41,6 +41,9 @@ const (
41 // ServerServiceGetSigningKeyProcedure is the fully-qualified name of the ServerService's41 // ServerServiceGetSigningKeyProcedure is the fully-qualified name of the ServerService's
42 // GetSigningKey RPC.42 // GetSigningKey RPC.
43 ServerServiceGetSigningKeyProcedure = "/ermite.ServerService/GetSigningKey"43 ServerServiceGetSigningKeyProcedure = "/ermite.ServerService/GetSigningKey"
44+ // ServerServiceRotateOIDCKeyProcedure is the fully-qualified name of the ServerService's
45+ // RotateOIDCKey RPC.
46+ ServerServiceRotateOIDCKeyProcedure = "/ermite.ServerService/RotateOIDCKey"
44 )47 )
45 48
46 // ServerServiceClient is a client for the ermite.ServerService service.49 // ServerServiceClient is a client for the ermite.ServerService service.
@@ -55,6 +58,17 @@ type ServerServiceClient interface {
55 // Servers that implement it list "signing_key" in58 // Servers that implement it list "signing_key" in
56 // GetVersionResponse.features.59 // GetVersionResponse.features.
57 GetSigningKey(context.Context, *connect.Request[ermite.GetSigningKeyRequest]) (*connect.Response[ermite.GetSigningKeyResponse], error)60 GetSigningKey(context.Context, *connect.Request[ermite.GetSigningKeyRequest]) (*connect.Response[ermite.GetSigningKeyResponse], error)
61+ // RotateOIDCKey replaces the key with which the server signs the OIDC ID
62+ // tokens of CI jobs (Job.id_token_url) by a new one. The previous key stops
63+ // signing at once but stays in the server's JWKS
64+ // ("/-/oidc/.well-known/jwks") for 24 hours, so that tokens it signed still
65+ // verify until they expire. It needs owner rights: the admin token, the
66+ // owner's session, or a token with every scope and no repository list or
67+ // branch prefixes (permission_denied otherwise). It fails with
68+ // failed_precondition when the server issues no ID tokens (it has no
69+ // secrets key to seal the key with). Servers that implement it list
70+ // "ci.oidc" in GetVersionResponse.features.
71+ RotateOIDCKey(context.Context, *connect.Request[ermite.RotateOIDCKeyRequest]) (*connect.Response[ermite.RotateOIDCKeyResponse], error)
58 }72 }
59 73
60 // NewServerServiceClient constructs a client for the ermite.ServerService service. By default, it74 // NewServerServiceClient constructs a client for the ermite.ServerService service. By default, it
@@ -80,6 +94,12 @@ func NewServerServiceClient(httpClient connect.HTTPClient, baseURL string, opts
80 connect.WithSchema(serverServiceMethods.ByName("GetSigningKey")),94 connect.WithSchema(serverServiceMethods.ByName("GetSigningKey")),
81 connect.WithClientOptions(opts...),95 connect.WithClientOptions(opts...),
82 ),96 ),
97+ rotateOIDCKey: connect.NewClient[ermite.RotateOIDCKeyRequest, ermite.RotateOIDCKeyResponse](
98+ httpClient,
99+ baseURL+ServerServiceRotateOIDCKeyProcedure,
100+ connect.WithSchema(serverServiceMethods.ByName("RotateOIDCKey")),
101+ connect.WithClientOptions(opts...),
102+ ),
83 }103 }
84 }104 }
85 105
@@ -87,6 +107,7 @@ func NewServerServiceClient(httpClient connect.HTTPClient, baseURL string, opts
87 type serverServiceClient struct {107 type serverServiceClient struct {
88 getVersion *connect.Client[ermite.GetVersionRequest, ermite.GetVersionResponse]108 getVersion *connect.Client[ermite.GetVersionRequest, ermite.GetVersionResponse]
89 getSigningKey *connect.Client[ermite.GetSigningKeyRequest, ermite.GetSigningKeyResponse]109 getSigningKey *connect.Client[ermite.GetSigningKeyRequest, ermite.GetSigningKeyResponse]
110+ rotateOIDCKey *connect.Client[ermite.RotateOIDCKeyRequest, ermite.RotateOIDCKeyResponse]
90 }111 }
91 112
92 // GetVersion calls ermite.ServerService.GetVersion.113 // GetVersion calls ermite.ServerService.GetVersion.
@@ -99,6 +120,11 @@ func (c *serverServiceClient) GetSigningKey(ctx context.Context, req *connect.Re
99 return c.getSigningKey.CallUnary(ctx, req)120 return c.getSigningKey.CallUnary(ctx, req)
100 }121 }
101 122
123+// RotateOIDCKey calls ermite.ServerService.RotateOIDCKey.
124+func (c *serverServiceClient) RotateOIDCKey(ctx context.Context, req *connect.Request[ermite.RotateOIDCKeyRequest]) (*connect.Response[ermite.RotateOIDCKeyResponse], error) {
125+ return c.rotateOIDCKey.CallUnary(ctx, req)
126+}
127+
102 // ServerServiceHandler is an implementation of the ermite.ServerService service.128 // ServerServiceHandler is an implementation of the ermite.ServerService service.
103 type ServerServiceHandler interface {129 type ServerServiceHandler interface {
104 // GetVersion returns the server version and the optional capabilities of the130 // GetVersion returns the server version and the optional capabilities of the
@@ -111,6 +137,17 @@ type ServerServiceHandler interface {
111 // Servers that implement it list "signing_key" in137 // Servers that implement it list "signing_key" in
112 // GetVersionResponse.features.138 // GetVersionResponse.features.
113 GetSigningKey(context.Context, *connect.Request[ermite.GetSigningKeyRequest]) (*connect.Response[ermite.GetSigningKeyResponse], error)139 GetSigningKey(context.Context, *connect.Request[ermite.GetSigningKeyRequest]) (*connect.Response[ermite.GetSigningKeyResponse], error)
140+ // RotateOIDCKey replaces the key with which the server signs the OIDC ID
141+ // tokens of CI jobs (Job.id_token_url) by a new one. The previous key stops
142+ // signing at once but stays in the server's JWKS
143+ // ("/-/oidc/.well-known/jwks") for 24 hours, so that tokens it signed still
144+ // verify until they expire. It needs owner rights: the admin token, the
145+ // owner's session, or a token with every scope and no repository list or
146+ // branch prefixes (permission_denied otherwise). It fails with
147+ // failed_precondition when the server issues no ID tokens (it has no
148+ // secrets key to seal the key with). Servers that implement it list
149+ // "ci.oidc" in GetVersionResponse.features.
150+ RotateOIDCKey(context.Context, *connect.Request[ermite.RotateOIDCKeyRequest]) (*connect.Response[ermite.RotateOIDCKeyResponse], error)
114 }151 }
115 152
116 // NewServerServiceHandler builds an HTTP handler from the service implementation. It returns the153 // NewServerServiceHandler builds an HTTP handler from the service implementation. It returns the
@@ -132,12 +169,20 @@ func NewServerServiceHandler(svc ServerServiceHandler, opts ...connect.HandlerOp
132 connect.WithSchema(serverServiceMethods.ByName("GetSigningKey")),169 connect.WithSchema(serverServiceMethods.ByName("GetSigningKey")),
133 connect.WithHandlerOptions(opts...),170 connect.WithHandlerOptions(opts...),
134 )171 )
172+ serverServiceRotateOIDCKeyHandler := connect.NewUnaryHandler(
173+ ServerServiceRotateOIDCKeyProcedure,
174+ svc.RotateOIDCKey,
175+ connect.WithSchema(serverServiceMethods.ByName("RotateOIDCKey")),
176+ connect.WithHandlerOptions(opts...),
177+ )
135 return "/ermite.ServerService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {178 return "/ermite.ServerService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
136 switch r.URL.Path {179 switch r.URL.Path {
137 case ServerServiceGetVersionProcedure:180 case ServerServiceGetVersionProcedure:
138 serverServiceGetVersionHandler.ServeHTTP(w, r)181 serverServiceGetVersionHandler.ServeHTTP(w, r)
139 case ServerServiceGetSigningKeyProcedure:182 case ServerServiceGetSigningKeyProcedure:
140 serverServiceGetSigningKeyHandler.ServeHTTP(w, r)183 serverServiceGetSigningKeyHandler.ServeHTTP(w, r)
184+ case ServerServiceRotateOIDCKeyProcedure:
185+ serverServiceRotateOIDCKeyHandler.ServeHTTP(w, r)
141 default:186 default:
142 http.NotFound(w, r)187 http.NotFound(w, r)
143 }188 }
@@ -154,3 +199,7 @@ func (UnimplementedServerServiceHandler) GetVersion(context.Context, *connect.Re
154 func (UnimplementedServerServiceHandler) GetSigningKey(context.Context, *connect.Request[ermite.GetSigningKeyRequest]) (*connect.Response[ermite.GetSigningKeyResponse], error) {199 func (UnimplementedServerServiceHandler) GetSigningKey(context.Context, *connect.Request[ermite.GetSigningKeyRequest]) (*connect.Response[ermite.GetSigningKeyResponse], error) {
155 return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.ServerService.GetSigningKey is not implemented"))200 return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.ServerService.GetSigningKey is not implemented"))
156 }201 }
202+
203+func (UnimplementedServerServiceHandler) RotateOIDCKey(context.Context, *connect.Request[ermite.RotateOIDCKeyRequest]) (*connect.Response[ermite.RotateOIDCKeyResponse], error) {
204+ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.ServerService.RotateOIDCKey is not implemented"))
205+}
M gen/ermite/runner.pb.go +46 -5 generated file: collapsed
M gen/ermite/server.pb.go +144 -14 generated file: collapsed
M gen_test.go +1 -0
@@ -47,6 +47,7 @@ func TestProcedures(t *testing.T) {
4747 {ermiteconnect.SessionServiceSignOutEverywhereProcedure, "/ermite.SessionService/SignOutEverywhere"},
4848 {ermiteconnect.ServerServiceGetVersionProcedure, "/ermite.ServerService/GetVersion"},
4949 {ermiteconnect.ServerServiceGetSigningKeyProcedure, "/ermite.ServerService/GetSigningKey"},
50+ {ermiteconnect.ServerServiceRotateOIDCKeyProcedure, "/ermite.ServerService/RotateOIDCKey"},
5051 {ermiteconnect.RunnerServiceRegisterRunnerProcedure, "/ermite.RunnerService/RegisterRunner"},
5152 {ermiteconnect.RunnerServiceFetchJobProcedure, "/ermite.RunnerService/FetchJob"},
5253 {ermiteconnect.RunnerServiceUpdateJobProcedure, "/ermite.RunnerService/UpdateJob"},
@@ -47,6 +47,7 @@ func TestProcedures(t *testing.T) {
47 {ermiteconnect.SessionServiceSignOutEverywhereProcedure, "/ermite.SessionService/SignOutEverywhere"},47 {ermiteconnect.SessionServiceSignOutEverywhereProcedure, "/ermite.SessionService/SignOutEverywhere"},
48 {ermiteconnect.ServerServiceGetVersionProcedure, "/ermite.ServerService/GetVersion"},48 {ermiteconnect.ServerServiceGetVersionProcedure, "/ermite.ServerService/GetVersion"},
49 {ermiteconnect.ServerServiceGetSigningKeyProcedure, "/ermite.ServerService/GetSigningKey"},49 {ermiteconnect.ServerServiceGetSigningKeyProcedure, "/ermite.ServerService/GetSigningKey"},
50+ {ermiteconnect.ServerServiceRotateOIDCKeyProcedure, "/ermite.ServerService/RotateOIDCKey"},
50 {ermiteconnect.RunnerServiceRegisterRunnerProcedure, "/ermite.RunnerService/RegisterRunner"},51 {ermiteconnect.RunnerServiceRegisterRunnerProcedure, "/ermite.RunnerService/RegisterRunner"},
51 {ermiteconnect.RunnerServiceFetchJobProcedure, "/ermite.RunnerService/FetchJob"},52 {ermiteconnect.RunnerServiceFetchJobProcedure, "/ermite.RunnerService/FetchJob"},
52 {ermiteconnect.RunnerServiceUpdateJobProcedure, "/ermite.RunnerService/UpdateJob"},53 {ermiteconnect.RunnerServiceUpdateJobProcedure, "/ermite.RunnerService/UpdateJob"},
M proto/ermite/runner.proto +24 -0
@@ -597,6 +597,30 @@ message Job {
597597 // this in the log of "Set up job". False from servers that do not send it:
598598 // the safe reading.
599599 bool trusted = 21;
600+ // IdTokenUrl is the URL the job's code fetches OIDC ID tokens from, the
601+ // ACTIONS_ID_TOKEN_REQUEST_URL of GitHub's runner: the server's
602+ // "/-/oidc/token?api-version=2.0". It already has a query, so clients append
603+ // "&audience=<url-encoded audience>" as @actions/core does. A GET with
604+ // "Authorization: Bearer <id_token_request_token>" answers
605+ // {"value":"<jwt>"}: an RS256 JWT with GitHub's claims, signed by the key
606+ // the server publishes at "/-/oidc/.well-known/jwks". The runner sets
607+ // ACTIONS_ID_TOKEN_REQUEST_URL to it for run steps, JavaScript actions and
608+ // Docker actions, with the origin rewritten as for the other server URLs
609+ // the sandbox reaches. Empty when the job may not have ID tokens: when it is
610+ // not trusted, when its effective permissions do not include
611+ // "id-token: write", when the server has no key to sign with, and from
612+ // servers without the feature "ci.oidc". Both fields are set or neither.
613+ string id_token_url = 22;
614+ // IdTokenRequestToken is the bearer for id_token_url, the
615+ // ACTIONS_ID_TOKEN_REQUEST_TOKEN of GitHub's runner: "ermiteoidc_" and 43
616+ // base64url characters (32 random bytes, no padding). It is valid only for
617+ // id_token_url, only while the job is assigned or running (it is revoked
618+ // when the job ends, on cancellation and when the job is lost), and is
619+ // shown only in this response, or in a repeat of the same poll_id. It is a
620+ // secret: the runner registers it in its masker before anything runs, and
621+ // gives it to the sandbox only with id_token_url. Empty when id_token_url
622+ // is.
623+ string id_token_request_token = 23;
600624 }
601625
602626 // JobPermissions are the permissions of the job repository token, with the
@@ -597,6 +597,30 @@ message Job {
597 // this in the log of "Set up job". False from servers that do not send it:597 // this in the log of "Set up job". False from servers that do not send it:
598 // the safe reading.598 // the safe reading.
599 bool trusted = 21;599 bool trusted = 21;
600+ // IdTokenUrl is the URL the job's code fetches OIDC ID tokens from, the
601+ // ACTIONS_ID_TOKEN_REQUEST_URL of GitHub's runner: the server's
602+ // "/-/oidc/token?api-version=2.0". It already has a query, so clients append
603+ // "&audience=<url-encoded audience>" as @actions/core does. A GET with
604+ // "Authorization: Bearer <id_token_request_token>" answers
605+ // {"value":"<jwt>"}: an RS256 JWT with GitHub's claims, signed by the key
606+ // the server publishes at "/-/oidc/.well-known/jwks". The runner sets
607+ // ACTIONS_ID_TOKEN_REQUEST_URL to it for run steps, JavaScript actions and
608+ // Docker actions, with the origin rewritten as for the other server URLs
609+ // the sandbox reaches. Empty when the job may not have ID tokens: when it is
610+ // not trusted, when its effective permissions do not include
611+ // "id-token: write", when the server has no key to sign with, and from
612+ // servers without the feature "ci.oidc". Both fields are set or neither.
613+ string id_token_url = 22;
614+ // IdTokenRequestToken is the bearer for id_token_url, the
615+ // ACTIONS_ID_TOKEN_REQUEST_TOKEN of GitHub's runner: "ermiteoidc_" and 43
616+ // base64url characters (32 random bytes, no padding). It is valid only for
617+ // id_token_url, only while the job is assigned or running (it is revoked
618+ // when the job ends, on cancellation and when the job is lost), and is
619+ // shown only in this response, or in a repeat of the same poll_id. It is a
620+ // secret: the runner registers it in its masker before anything runs, and
621+ // gives it to the sandbox only with id_token_url. Empty when id_token_url
622+ // is.
623+ string id_token_request_token = 23;
600 }624 }
601 625
602 // JobPermissions are the permissions of the job repository token, with the626 // JobPermissions are the permissions of the job repository token, with the
M proto/ermite/server.proto +33 -0
@@ -4,6 +4,8 @@ syntax = "proto3";
44
55 package ermite;
66
7+import "google/protobuf/timestamp.proto";
8+
79 // ServerService describes the server itself.
810 service ServerService {
911 // GetVersion returns the server version and the optional capabilities of the
@@ -16,6 +18,17 @@ service ServerService {
1618 // Servers that implement it list "signing_key" in
1719 // GetVersionResponse.features.
1820 rpc GetSigningKey(GetSigningKeyRequest) returns (GetSigningKeyResponse);
21+ // RotateOIDCKey replaces the key with which the server signs the OIDC ID
22+ // tokens of CI jobs (Job.id_token_url) by a new one. The previous key stops
23+ // signing at once but stays in the server's JWKS
24+ // ("/-/oidc/.well-known/jwks") for 24 hours, so that tokens it signed still
25+ // verify until they expire. It needs owner rights: the admin token, the
26+ // owner's session, or a token with every scope and no repository list or
27+ // branch prefixes (permission_denied otherwise). It fails with
28+ // failed_precondition when the server issues no ID tokens (it has no
29+ // secrets key to seal the key with). Servers that implement it list
30+ // "ci.oidc" in GetVersionResponse.features.
31+ rpc RotateOIDCKey(RotateOIDCKeyRequest) returns (RotateOIDCKeyResponse);
1932 }
2033
2134 // GetVersionRequest is empty.
@@ -71,3 +84,23 @@ message GetSigningKeyResponse {
7184 // "git config gpg.ssh.allowedSignersFile".
7285 string allowed_signers = 5;
7386 }
87+
88+// RotateOIDCKeyRequest is empty.
89+message RotateOIDCKeyRequest {}
90+
91+// RotateOIDCKeyResponse describes the new signing key of the ID tokens.
92+message RotateOIDCKeyResponse {
93+ // Issuer is the "iss" of the ID tokens, for example
94+ // "https://git.example.com/-/oidc". Its discovery document is at
95+ // "<issuer>/.well-known/openid-configuration".
96+ string issuer = 1;
97+ // Kid is the key id of the new key, the "kid" of the tokens it signs: the
98+ // RFC 7638 thumbprint of its public key, base64url without padding.
99+ string kid = 2;
100+ // RetiredKid is the key id of the key it replaces; empty when there was
101+ // none.
102+ string retired_kid = 3;
103+ // RetiredUntil is when the replaced key leaves the JWKS. Unset when there
104+ // was none.
105+ google.protobuf.Timestamp retired_until = 4;
106+}
@@ -4,6 +4,8 @@ syntax = "proto3";
4 4
5 package ermite;5 package ermite;
6 6
7+import "google/protobuf/timestamp.proto";
8+
7 // ServerService describes the server itself.9 // ServerService describes the server itself.
8 service ServerService {10 service ServerService {
9 // GetVersion returns the server version and the optional capabilities of the11 // GetVersion returns the server version and the optional capabilities of the
@@ -16,6 +18,17 @@ service ServerService {
16 // Servers that implement it list "signing_key" in18 // Servers that implement it list "signing_key" in
17 // GetVersionResponse.features.19 // GetVersionResponse.features.
18 rpc GetSigningKey(GetSigningKeyRequest) returns (GetSigningKeyResponse);20 rpc GetSigningKey(GetSigningKeyRequest) returns (GetSigningKeyResponse);
21+ // RotateOIDCKey replaces the key with which the server signs the OIDC ID
22+ // tokens of CI jobs (Job.id_token_url) by a new one. The previous key stops
23+ // signing at once but stays in the server's JWKS
24+ // ("/-/oidc/.well-known/jwks") for 24 hours, so that tokens it signed still
25+ // verify until they expire. It needs owner rights: the admin token, the
26+ // owner's session, or a token with every scope and no repository list or
27+ // branch prefixes (permission_denied otherwise). It fails with
28+ // failed_precondition when the server issues no ID tokens (it has no
29+ // secrets key to seal the key with). Servers that implement it list
30+ // "ci.oidc" in GetVersionResponse.features.
31+ rpc RotateOIDCKey(RotateOIDCKeyRequest) returns (RotateOIDCKeyResponse);
19 }32 }
20 33
21 // GetVersionRequest is empty.34 // GetVersionRequest is empty.
@@ -71,3 +84,23 @@ message GetSigningKeyResponse {
71 // "git config gpg.ssh.allowedSignersFile".84 // "git config gpg.ssh.allowedSignersFile".
72 string allowed_signers = 5;85 string allowed_signers = 5;
73 }86 }
87+
88+// RotateOIDCKeyRequest is empty.
89+message RotateOIDCKeyRequest {}
90+
91+// RotateOIDCKeyResponse describes the new signing key of the ID tokens.
92+message RotateOIDCKeyResponse {
93+ // Issuer is the "iss" of the ID tokens, for example
94+ // "https://git.example.com/-/oidc". Its discovery document is at
95+ // "<issuer>/.well-known/openid-configuration".
96+ string issuer = 1;
97+ // Kid is the key id of the new key, the "kid" of the tokens it signs: the
98+ // RFC 7638 thumbprint of its public key, base64url without padding.
99+ string kid = 2;
100+ // RetiredKid is the key id of the key it replaces; empty when there was
101+ // none.
102+ string retired_kid = 3;
103+ // RetiredUntil is when the replaced key leaves the JWKS. Unset when there
104+ // was none.
105+ google.protobuf.Timestamp retired_until = 4;
106+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 18

↑↓ moveenter openesc close