1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463
|
# Access to Kubernetes from CI
## Problem to solve
As an Application Operator, I would like certain CI jobs to be able to access my Kubernetes cluster, connected
via GitLab Agent. That way I don't have to open up my cluster to access it from CI.
## Intended users
* [Allison (Application Ops)](https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/#allison-application-ops)
* [Priyanka (Platform Engineer)](https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/#priyanka-platform-engineer)
## User experience goal
The user can allow certain CI jobs to access Kubernetes clusters connected via the GitLab Agent.
A single CI job can access multiple clusters, that is to access multiple Agents.
This is often required in production environments, where the production environment is composed of multiple clusters
in different regions/availability zones.
## Proposal
In the Agent's configuration file, managed as code in the configuration project, user specifies a list of
projects and groups, CI jobs from which can access this particular agent. CI jobs of the configuration project itself
can access all agents configured via this project (TODO security review).
```yaml
# .gitlab/agents/my-agent/config.yaml
ci_access:
# This agent is accessible from CI jobs in these projects
projects:
- id: group1/group1-1/project1
default_namespace: namespace-to-use-as-default
environments:
- staging
- review/*
access_as:
agent: {}
impersonate:
username: name-of-identity-to-impersonate
uid: 06f6ce97-e2c5-4ab8-7ba5-7654dd08d52b
groups:
- group1
- group2
extra:
- key: key1
val: ["val1", "val2"]
- key: key2
val: ["x"]
ci_job: {}
ci_user: {}
# This agent is accessible from CI jobs in projects in these groups
groups:
- id: group2/group2-1
default_namespace: ...
environments: ...
access_as: ...
```
When a CI job, that has access to one or more agents, runs, GitLab injects a
[`kubectl`-compatible configuration file](https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig)
(using a [variable of type `File`](https://docs.gitlab.com/ee/ci/variables/#custom-cicd-variables)) and sets
[`KUBECONFIG` environment variable](https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/#the-kubeconfig-environment-variable)
to its location on disk. The file contains a
[context](https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/#context)
per GitLab Agent that this CI job is allowed to access.
The `ci_access.projects[].default_namespace` specifies the namespace for the context used in the CI/CD tunnel. Omitting `default_namespace` does not set a namespace in the context.
`ci_access.projects[].environments[]` restricts agent usage to CI jobs that deploy to a matching
[environment](https://docs.gitlab.com/ee/ci/environments/). See [Branch and Environment restrictions](#branch-and-environment-restrictions).
If the project, where the CI job is running, has certificate-based integration configured, then the generated
configuration file contains contexts for both integrations. This allows users to use both integration
simultaneously, for example to migrate from one to the other.
CI job can set context `<context name>` as the current one using `kubectl config use-context <context name>`.
A context can also be explicitly specified in each `kubectl` invocation using `kubectl --context=<context name> <command>`.
After a context is selected, `kubectl` (or any other compatible program) can be used as if working with a cluster directly.
We might add another level of authorization from the group side, if requested by users. This is tracked by
https://gitlab.com/gitlab-org/gitlab/-/issues/330591 and is initially out of scope for the CI tunnel.
## Implementation
### `kubectl` configuration file
- [`Context name`](https://github.com/kubernetes/client-go/blob/v0.20.4/tools/clientcmd/api/v1/types.go#L165) is
constructed according to the following pattern: `<configuration project full path>:<agent name>`.
Example: `groupX/subgroup1/project1:my-agent`.
- [`Server`](https://github.com/kubernetes/client-go/blob/v0.20.4/tools/clientcmd/api/v1/types.go#L65) is set to
`https://kas.gitlabhost.tld:<port>`. There needs to be only one
[`NamedCluster`](https://github.com/kubernetes/client-go/blob/v0.20.4/tools/clientcmd/api/v1/types.go#L155)
element in the config that all contexts refer to. It's
[`Name`](https://github.com/kubernetes/client-go/blob/v0.20.4/tools/clientcmd/api/v1/types.go#L157) should be set to
`gitlab`.
- [`Namespace`](https://github.com/kubernetes/client-go/blob/v0.20.4/tools/clientcmd/api/v1/types.go#L148) is set to
the value of `projects[].default_namespace`.
- [`Token`](https://github.com/kubernetes/client-go/blob/v0.20.4/tools/clientcmd/api/v1/types.go#L110) is set to the
value of `<token type>:<agent id>:<CI_JOB_TOKEN>`, where:
- `<token type>` is the type of the token that is being provided. For CI integration it's the string `ci`. In the
future we may have more types of tokens that `gitlab-kas` may accept.
- `<agent id>` is the id of the agent that can be accessed using this context. This
value and the context's name are the only unique values across contexts.
- `<CI_JOB_TOKEN>` is the value of the
[`CI_JOB_TOKEN`](https://docs.gitlab.com/ee/user/project/new_ci_build_permissions_model.html#job-token) variable.
### Branch and Environment restrictions
When `ci_access.projects[].environments[]` is present in an agent's configuration, only CI jobs that deploy
to a matching [environment](https://docs.gitlab.com/ee/ci/environments/) are allowed to use the agent.
One or more environment entries can be specified, where each contains either the environment name or a wildcard
[environment scope](https://docs.gitlab.com/ee/ci/environments/#scope-environments-with-specs). If the CI job's
environment does not match any entries:
- The injected configuration does not have a context for the agent.
- The `allowed_agents` API response does not include the agent.
Environments can also be specified at the group level with `ci_access.groups[].environments[]`. If a project is
authorized multiple times (for example, at both the project and group level), only the most specific configuration
is used and environment entries are not merged. See [`/api/v4/job/allowed_agents` API](#apiv4joballowed_agents-api)
for details on configuration specificity.
### Identifiers
All identifiers have one of the following structures:
- `gitlab:<identifier type>`
- `gitlab:<identifier type>:<identifier type-specific information>`. `identifier type-specific information` may contain
columns (`:`) to separate pieces of information.
### Impersonation
User [impersonation](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#user-impersonation),
when configured, supplies identifying information to the in-cluster access control mechanisms,
such as RBAC and admission controllers, when a request is made. This allows Platform Engineers to precisely set up
permissions based on groups and/or "extra".
Identity that is used to make an actual Kubernetes API request in a cluster is configured using
the `access_as` config section. For any option other than `agent` to work, `agentk`'s
`ServiceAccount` needs to have correct permissions. At most one key is allowed:
- `agent` - make the requests using the agent's identity i.e. using the `ServiceAccount` credentials the
`agentk` `Pod` is running under. This is the default behavior. This is the only impersonation mode where user
can use the impersonation functionality from the client. In other modes requests with impersonation headers are
rejected with 400 because they can not be fulfilled - those headers are already in use by the impersonation mode and
there is no way to perform "nested impersonation".
- `impersonate` - make the requests using some identity.
- `ci_job` - impersonate the CI job. When the agent makes the request to the actual Kubernetes API, it sets the
impersonation credentials in the following way:
- `UserName` is set to `gitlab:ci_job:<job id>`
Example: `gitlab:ci_job:1074499489`.
- `Groups` is set to:
- `gitlab:ci_job` to identify all requests coming from CI jobs.
- The list of ids of groups the project is in.
- The project id.
- The slug of the environment this job belongs to.
Example: for a CI job in `group1/group1-1/project1` where:
- Group `group1` has id `23`.
- Group `group1/group1-1` has id `25`.
- Project `group1/group1-1/project1` has id `150`.
- Job running in a `prod` environment, which has the `production` environment tier.
group list would be [`gitlab:ci_job`, `gitlab:group:23`, `gitlab:group_env_tier:23:production`, `gitlab:group:25`, `gitlab:group_env_tier:25:production`,
`gitlab:project:150`, `gitlab:project_env:150:prod`, `gitlab:project_env_tier:150:production`].
- `Extra` carries extra information about the request:
- `agent.gitlab.com/id` contains the agent id.
- `agent.gitlab.com/config_project_id` contains the agent's configuration project id.
- `agent.gitlab.com/project_id` contains the CI project id.
- `agent.gitlab.com/ci_pipeline_id` contains the CI pipeline id.
- `agent.gitlab.com/ci_job_id` contains the CI job id.
- `agent.gitlab.com/username` contains the username of the user the CI job is running as.
- `agent.gitlab.com/environment_slug` contains the slug of the environment. Only set if running in an environment.
- `agent.gitlab.com/environment_tier` contains the deployment tier of the environment. Only set if running in an environment.
- `ci_user` - impersonate the user this CI job is running as. Details depend on
https://gitlab.com/gitlab-org/gitlab/-/issues/243740, tentatively:
- `UserName` is set to `gitlab:user:<username>`
Example: `gitlab:user:ash2k`.
- `Groups` is set to:
- `gitlab:user` to identify all requests coming from GitLab users.
- The list of roles the user has in the project where the CI job is running.
Example: for a Maintainer in project `group1/group1-1/project1` with id `150`
the list of groups would be [`gitlab:user`, `gitlab:project_role:150:reporter`, `gitlab:project_role:150:developer`,
`gitlab:project_role:150:maintainer`]
- `Extra` - see above.
Full list of groups for a user can be huge, so it was decided to use a list of roles the user has instead.
Group/project ids are used because:
- group/project names can be sensitive information that should not be exposed.
- group/project names can change over time, breaking permissions set in RBAC.
### Authentication
Requests to `https://kas.gitlabhost.tld:<port>` are authenticated using the `CI_JOB_TOKEN` that is passed in each request.
### Authorization
There are two authorization steps, performed in the following order:
1. Coarse-grained authorization: the CI job, identified by the supplied `CI_JOB_TOKEN`, is checked to see if it is
allowed to access a particular agent, identified by the supplied agent id.
Note that any agent id can be supplied by manipulating the
configuration file, but only the agent ids that are allowed to be accessed from that particular CI job are allowed to
pass this authorization step.
1. Fine-grained authorization: performed by the in-cluster access control mechanisms, configured by the Platform
Engineer. Information, described in the [Impersonation](#impersonation) section above, can be used to define what is allowed.
### Default configuration
Be default, the agent should work without an agent configuration file as well. The following configuration should be
the default:
```yaml
# .gitlab/agents/<agent name>/config.yaml
ci_access:
projects:
- id: "<agent's configuration project id>"
access_as:
agent: {}
```
### Notifying GitLab of agent's configuration
According to the [proposal](#proposal), user maintains the list of groups and/or projects
in the agent's configuration file. This can be thought of as `agent id` -> `allowed project id` and
`agent id` -> `allowed group id` indexes. We need **reverse** of these i.e. information about agents, allowed for a
project/group to access. It is needed to:
- Implement the [`/api/v4/job/allowed_agents`](#apiv4joballowed_agents-api) API endpoint, providing the list of
allowed agents with their configuration.
- To be able to construct the `kubectl` configuration file.
https://gitlab.com/gitlab-org/gitlab/-/issues/323708 tracks the plumbing work to make it possible to build such an index.
Once it is implemented, we need to add new indexes to be able to perform:
- `ci project id` -> `agent id` lookups: https://gitlab.com/gitlab-org/gitlab/-/issues/327411
- `group id` -> `agent id` lookups: https://gitlab.com/gitlab-org/gitlab/-/issues/327851
### `/api/v4/job/allowed_agents` API
`/api/v4/job/allowed_agents` is a new endpoint that returns the required data:
- Information about the CI job, pipeline, project, user.
- The list of agent ids that this CI job is allowed to access.
Only the needed fields are returned, not everything. Algorithm:
1. Retrieve the list of agents allowed to be accessed from the CI project by querying the
`ci project id` -> `agent id` index.
1. Retrieve the list of agents configured in the CI project, if any. These are allowed to be accessed from CI jobs
implicitly with default configuration. The user can set configuration by explicitly granting access to the
configuration project - to allow that, explicit grants are prioritized over implicit configuration.
1. Gather an ordered (from more nested/inner to less nested/outer) list of groups for the CI project by querying the
`group id` -> `agent id` index.
Example: for project `group1/group1-1/project1` the list would be [`group1/group1-1`, `group1`].
1. For each group fetch the list of agents, allowed to be accessed by that group. If an agent id has already been seen
either on step 1 or this step, discard the found information. Keep the most specific configuration for the agent.
Example: for project `group1/group1-1/project1` the configuration specificity order is:
1. Project-level configuration `group1/group1-1/project1`.
1. Inner-most group configuration `group1/group1-1`.
1. Outer group configuration `group1`.
1. **TBD** What happens if user grants access to a group, containing the agent configuration project? Does it override the
implicit configuration or not?
1. Collate information from above and return it.
Request:
```text
GET /api/v4/job/allowed_agents
Accept: application/json
Job-Token: <CI_JOB_TOKEN>
```
`Job-Token` header name is consistent with other API endpoints that use `CI_JOB_TOKEN` for authentication.
Response on success:
```text
HTTP/1.1 200 OK
Content-Type: application/json
{
"allowed_agents": [
{
"id": 5, // agent id
"config_project": {
"id": 3
},
"configuration": { // contains section of the agent's config file as is, with 'id' removed
"default_namespace": "namespace-to-use-as-default",
"access_as": {
"agent: {}
}
}
},
{
"id": 3,
"config_project": {
"id": 3 // same as above
},
"configuration": {
// "default_namespace": "", // not set
"access_as": {
"ci_job: {}
}
}
},
{
"id": 10,
"config_project": {
"id": 11 // agent from a different project
},
"configuration": {
"access_as": {
"ci_user: {}
}
}
}
],
"job": {
"id": 3 // job id
},
"pipeline": {
"id": 6 // pipeline id
},
"project": {
"id": 150, // project id
"groups": [
{
"id": 23 // id of the group this project is in
},
{
"id": 25
}
]
},
"environment": {
"slug": "slug_of_the_environment" // empty if not part of an environment
"tier": "deployment_tier_of_the_environment" // empty if not part of an environment
},
"user": { // user who is running the job
"id": 1,
"username": "root",
"roles_in_project": [
"reporter", "developer", "maintainer"
]
}
}
```
### `/api/v4/internal/kubernetes/agent_configuration` API
`/api/v4/internal/kubernetes/agent_configuration` is a new endpoint that accepts configuration for an agent and
updates necessary records in DB. It is invoked by `kas` each time it fetches an updated agent configuration.
If there is an error invoking the endpoint, `kas` still proceeds with returning the configuration to the agent to avoid
impacting the user if there is an internal communication issue.
`kas` might send the same configuration more than once because it sends it on each new commit, even if there are no
changes. This is consistent with `kas` sending configuration and GitOps manifests on each commit.
We may optimize all three later or handle this on the Rails side to avoid doing duplicate work and
causing unnecessary DB load. One option is to cache `agent id` -> `configuration hash` in Redis and
compare the new/cached hashes before making any queries to the DB. This is not in scope of this document.
Sending "duplicate" configuration has certain benefits:
- Simpler to implement.
- If for any reason DB is not in sync (e.g. network errors), it will be updated eventually (on next commit).
Request:
```text
POST /api/v4/internal/kubernetes/agent_configuration
Content-Type: application/json
Gitlab-Kas-Api-Request: JWT token
{
"agent_id": 5,
"agent_config": {} // ConfigurationFile in pkg/agentcfg/agentcfg.proto
}
```
Response on success:
```text
HTTP/1.1 204 No content
```
Errors:
- If JWT token is invalid or missing, a corresponding HTTP status code is returned (401/403).
- If agent id does not exist, HTTP status code 400 is returned.
### Request proxying flow
1. `gitlab-kas` gets a request from the CI job with `CI_JOB_TOKEN` and agent id in it.
- If `CI_JOB_TOKEN` is missing, the request is rejected with HTTP code 401.
- if agent id is missing or invalid, the request is rejected with HTTP code 400.
1. `gitlab-kas` makes a request to [`/api/v4/job/allowed_agents`](#apiv4joballowed_agents-api) endpoint to get the information about the
`CI_JOB_TOKEN` it received.
- It handles the HTTP status codes, returning 401/403 on 401/403 i.e. when `CI_JOB_TOKEN` is invalid.
1. `gitlab-kas` checks if the agent id it got in the request from the CI job is in the list it got from `/api/v4/job/allowed_agents`.
If it is not, the request is rejected with HTTP code 403.
1. (optional) `gitlab-kas` adds impersonation headers to the request based on the agent's configuration.
1. `gitlab-kas` proxies the request to the destination agent, identified by the agent id from the request.
See [`gitlab-kas` request routing](kas_request_routing.md) for information on how the request routing works.
|