Skip to content

Commit

Permalink
feat: add gitlab revoke option so vault doesn't revoke the token when…
Browse files Browse the repository at this point in the history
… the parent expires (#52)
  • Loading branch information
ilijamt committed Feb 23, 2024
1 parent 1825ae7 commit ccf9a51
Show file tree
Hide file tree
Showing 15 changed files with 608 additions and 471 deletions.
76 changes: 70 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,65 @@ Before we can use this plugin we need to create an access token that will have r

The current authentication model requires providing Vault with a Gitlab Token.

## Configuration

### Config

| Property | Required | Default value | Sensitive | Description |
|:-------------------------:|:--------:|:-------------:|:---------:|:----------------------------------------------------------------------------------------------------------------------------------|
| token | yes | n/a | yes | The token to access Gitlab API |
| base_url | yes | n/a | no | The address to access Gitlab |
| auto_rotate_token | no | no | no | Should we autorotate the token when it's close to expiry? (Experimental) |
| revoke_auto_rotated_token | no | no | no | Should we revoke the auto-rotated token after a new one has been generated? |
| auto_rotate_before | no | 24h | no | How much time should be remaining on the token validity before we should rotate it? Minimum can be set to 24h and maximum to 730h |

### Role

| Property | Required | Default value | Sensitive | Description |
|:--------------------:|:--------:|:-------------:|:---------:|:---------------------------------------------------------------------------------------------------------------------|
| path | yes | n/a | no | Project/Group path to create an access token for. If the token type is set to personal then write the username here. |
| name | yes | n/a | no | The name of the access token |
| ttl | yes | n/a | no | The TTL of the token |
| access_level | no/yes | n/a | no | Access level of access token (only required for Group and Project access tokens) |
| scopes | no | [] | no | List of scopes |
| token_type | yes | n/a | no | Access token type |
| gitlab_revokes_token | no | no | no | Gitlab revokes the token when it's time. Vault will not revoke the token when the lease expires |

#### ttl

Depending on `gitlab_revokes_token` the TTL will change.

* `true` - 24h <= ttl <= 365 days
* `false` - 1h <= ttl <= 365 days

#### access_level

It's not required if `token_type` is set to `personal`.

For a list of available roles check https://docs.gitlab.com/ee/user/permissions.html

#### scopes

Depending on the type of token you have different scopes:

* `Personal` - https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html#personal-access-token-scopes
* `Project` - https://docs.gitlab.com/ee/user/project/settings/project_access_tokens.html#scopes-for-a-project-access-token
* `Group` - https://docs.gitlab.com/ee/user/group/settings/group_access_tokens.html#scopes-for-a-group-access-token

#### token_types

Can be

* personal
* project
* group

#### gitlab_revokes_token

This is a flag that doesn't expire the token when the token used to create the credentials expire.
When the vault token used to create gitlab credentials with a TTL longer than the vault token, the new gitlab credentials will expire at the same time with the parent.
Setting this up will not call the revoke endpoint on gitlab.

## Examples

### Setup
Expand All @@ -56,16 +115,21 @@ Due to how Gitlab manages expiration the minimum is 24h and maximum is 365 days.
[Remove ability to create deprecated non-expiring access tokens](https://gitlab.com/gitlab-org/gitlab/-/issues/392855).
Since Gitlab 16.0 the ability to create non expiring token has been removed.

If you use Vault to manage the tokens the minimal TTL you can use is `1h`, by setting `gitlab_revokes_token=false`.

The command bellow will set up the config backend with a max TTL of 48h.

```shell
$ vault write gitlab/config max_ttl=48h base_url=https://gitlab.example.com token=gitlab-super-secret-token
$ vault write gitlab/config base_url=https://gitlab.example.com token=gitlab-super-secret-token
```

You may also need to configure the Max TTL for a token that can be issued by setting:
You may also need to configure the Max/Default TTL for a token that can be issued by setting:

Max TTL: 1 year
Default TTL: 1 week

```shell
$ vault secrets tune -max-lease-ttl=8784h gitlab/
$ vault secrets tune -max-lease-ttl=8784h -default-lease-ttl=168h gitlab/
```

Check https://developer.hashicorp.com/vault/docs/commands/secrets/tune for more information.
Expand All @@ -76,11 +140,11 @@ This will create three roles, one of each type.

```shell
# personal access tokens can only be created by Gitlab Administrators (see https://docs.gitlab.com/ee/api/users.html#create-a-personal-access-token)
$ vault write gitlab/roles/personal name=personal-token-name path=username scopes="read_api" token_type=personal token_ttl=24h
$ vault write gitlab/roles/personal name=personal-token-name path=username scopes="read_api" token_type=personal ttl=48h

$ vault write gitlab/roles/project name=project-token-name path=group/project scopes="read_api" access_level=guest token_type=project token_ttl=24h
$ vault write gitlab/roles/project name=project-token-name path=group/project scopes="read_api" access_level=guest token_type=project ttl=48h

$ vault write gitlab/roles/group name=group-token-name path=group/subgroup scopes="read_api" access_level=developer token_type=group token_ttl=24h
$ vault write gitlab/roles/group name=group-token-name path=group/subgroup scopes="read_api" access_level=developer token_type=group ttl=48h
```

### Get access tokens
Expand Down
13 changes: 5 additions & 8 deletions entry_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (
type EntryConfig struct {
BaseURL string `json:"base_url" structs:"base_url" mapstructure:"base_url"`
Token string `json:"token" structs:"token" mapstructure:"token"`
MaxTTL time.Duration `json:"max_ttl" structs:"max_ttl" mapstructure:"max_ttl"`
AutoRotateToken bool `json:"auto_rotate_token" structs:"auto_rotate_token" mapstructure:"auto_rotate_token"`
AutoRotateBefore time.Duration `json:"auto_rotate_before" structs:"auto_rotate_before" mapstructure:"auto_rotate_before"`
TokenExpiresAt time.Time `json:"token_expires_at" structs:"token_expires_at" mapstructure:"token_expires_at"`
Expand All @@ -24,13 +23,11 @@ func (e EntryConfig) LogicalResponseData() map[string]any {
}

return map[string]any{
"max_ttl": int64(e.MaxTTL / time.Second),
"base_url": e.BaseURL,
"token": e.Token,
"auto_rotate_token": e.AutoRotateToken,
"auto_rotate_before": e.AutoRotateBefore.String(),
"token_expires_at": tokenExpiresAt,
"revoke_auto_rotated_token": e.RevokeAutoRotatedToken,
"base_url": e.BaseURL,
"token": e.Token,
"auto_rotate_token": e.AutoRotateToken,
"auto_rotate_before": e.AutoRotateBefore.String(),
"token_expires_at": tokenExpiresAt,
}
}

Expand Down
30 changes: 16 additions & 14 deletions entry_role.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,26 @@ import (
)

type entryRole struct {
RoleName string `json:"role_name" structs:"role_name" mapstructure:"role_name"`
TokenTTL time.Duration `json:"token_ttl" structs:"token_ttl" mapstructure:"token_ttl"`
Path string `json:"path" structs:"path" mapstructure:"path"`
Name string `json:"name" structs:"name" mapstructure:"name"`
Scopes []string `json:"scopes" structs:"scopes" mapstructure:"scopes"`
AccessLevel AccessLevel `json:"access_level" structs:"access_level" mapstructure:"access_level,omitempty"`
TokenType TokenType `json:"token_type" structs:"token_type" mapstructure:"token_type"`
RoleName string `json:"role_name" structs:"role_name" mapstructure:"role_name"`
TTL time.Duration `json:"ttl" structs:"ttl" mapstructure:"ttl"`
Path string `json:"path" structs:"path" mapstructure:"path"`
Name string `json:"name" structs:"name" mapstructure:"name"`
Scopes []string `json:"scopes" structs:"scopes" mapstructure:"scopes"`
AccessLevel AccessLevel `json:"access_level" structs:"access_level" mapstructure:"access_level,omitempty"`
TokenType TokenType `json:"token_type" structs:"token_type" mapstructure:"token_type"`
GitlabRevokesTokens bool `json:"gitlab_revokes_token" structs:"gitlab_revokes_token" mapstructure:"gitlab_revokes_token"`
}

func (e entryRole) LogicalResponseData() map[string]any {
return map[string]any{
"role_name": e.RoleName,
"path": e.Path,
"name": e.Name,
"scopes": e.Scopes,
"access_level": e.AccessLevel.String(),
"token_ttl": int64(e.TokenTTL / time.Second),
"token_type": e.TokenType.String(),
"role_name": e.RoleName,
"path": e.Path,
"name": e.Name,
"scopes": e.Scopes,
"access_level": e.AccessLevel.String(),
"ttl": int64(e.TTL / time.Second),
"token_type": e.TokenType.String(),
"gitlab_revokes_token": e.GitlabRevokesTokens,
}
}

Expand Down
48 changes: 28 additions & 20 deletions entry_token.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
package gitlab

import "time"
import (
"strconv"
"time"
)

type EntryToken struct {
TokenID int `json:"token_id"`
UserID int `json:"user_id"`
ParentID string `json:"parent_id"`
Path string `json:"path"`
Name string `json:"name"`
Token string `json:"token"`
TokenType TokenType `json:"token_type"`
CreatedAt *time.Time `json:"created_at"`
ExpiresAt *time.Time `json:"expires_at"`
Scopes []string `json:"scopes"`
AccessLevel AccessLevel `json:"access_level"` // not used for personal access tokens
TokenID int `json:"token_id"`
UserID int `json:"user_id"`
ParentID string `json:"parent_id"`
Path string `json:"path"`
Name string `json:"name"`
Token string `json:"token"`
TokenType TokenType `json:"token_type"`
CreatedAt *time.Time `json:"created_at"`
ExpiresAt *time.Time `json:"expires_at"`
Scopes []string `json:"scopes"`
AccessLevel AccessLevel `json:"access_level"` // not used for personal access tokens
RoleName string `json:"role_name"`
GitlabRevokesToken bool `json:"gitlab_revokes_token"`
}

func (e EntryToken) SecretResponse() (map[string]any, map[string]any) {
Expand All @@ -22,18 +27,21 @@ func (e EntryToken) SecretResponse() (map[string]any, map[string]any) {
"token": e.Token,
"path": e.Path,
"scopes": e.Scopes,
"role_name": e.RoleName,
"access_level": e.AccessLevel.String(),
"created_at": e.CreatedAt,
"expires_at": e.ExpiresAt,
},
map[string]any{
"path": e.Path,
"name": e.Name,
"user_id": e.UserID,
"parent_id": e.ParentID,
"token_id": e.TokenID,
"token_type": e.TokenType.String(),
"scopes": e.Scopes,
"access_level": e.AccessLevel.String(),
"path": e.Path,
"name": e.Name,
"user_id": e.UserID,
"parent_id": e.ParentID,
"token_id": e.TokenID,
"token_type": e.TokenType.String(),
"scopes": e.Scopes,
"access_level": e.AccessLevel.String(),
"role_name": e.RoleName,
"gitlab_revokes_token": strconv.FormatBool(e.GitlabRevokesToken),
}
}
32 changes: 3 additions & 29 deletions path_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,8 @@ var (
},
"base_url": {
Type: framework.TypeString,
Description: `The address to access Gitlab. Default is "https://gitlab.com".`,
Default: "https://gitlab.com",
},
"max_ttl": {
Type: framework.TypeDurationSecond,
Description: `Maximum lifetime expected generated token will be valid for. If set to 0 it will be set for maximum 8670 hours`,
Default: DefaultConfigFieldAccessTokenMaxTTL,
Required: true,
Description: `The address to access Gitlab.`,
},
"auto_rotate_token": {
Type: framework.TypeBool,
Expand Down Expand Up @@ -109,7 +104,6 @@ func (b *Backend) pathConfigRead(ctx context.Context, req *logical.Request, data

func (b *Backend) pathConfigWrite(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
var warnings []string
var maxTtlRaw, maxTtlOk = data.GetOk("max_ttl")
var autoTokenRotateRaw, autoTokenRotateTtlOk = data.GetOk("auto_rotate_before")
var token, tokenOk = data.GetOk("token")
var err error
Expand All @@ -124,25 +118,6 @@ func (b *Backend) pathConfigWrite(ctx context.Context, req *logical.Request, dat
RevokeAutoRotatedToken: data.Get("revoke_auto_rotated_token").(bool),
}

if maxTtlOk {
maxTtl := maxTtlRaw.(int)
switch {
case maxTtl > 0 && maxTtl < int(DefaultAccessTokenMinTTL.Seconds()):
warnings = append(warnings, "max_ttl is set with less than 24 hours. With current token expiry limitation, this max_ttl is ignored, it's set to 24 hours")
config.MaxTTL = DefaultAccessTokenMinTTL
case maxTtl <= 0:
config.MaxTTL = DefaultAccessTokenMaxPossibleTTL
warnings = append(warnings, "max_ttl is not set. Token wil be generated with expiration date of '8760 hours'")
case maxTtl > int(DefaultAccessTokenMaxPossibleTTL.Seconds()):
warnings = append(warnings, "max_ttl is set to more than '8760 hours'. Token wil be generated with expiration date of '8760 hours'")
config.MaxTTL = DefaultAccessTokenMaxPossibleTTL
default:
config.MaxTTL = time.Duration(maxTtl) * time.Second
}
} else if config.MaxTTL == 0 {
config.MaxTTL = DefaultAccessTokenMaxPossibleTTL
}

if autoTokenRotateTtlOk {
atr, _ := convertToInt(autoTokenRotateRaw)
if atr > int(DefaultAutoRotateBeforeMaxTTL.Seconds()) {
Expand Down Expand Up @@ -173,15 +148,14 @@ func (b *Backend) pathConfigWrite(ctx context.Context, req *logical.Request, dat

event(ctx, b.Backend, "config-write", map[string]string{
"path": "config",
"max_ttl": config.MaxTTL.String(),
"auto_rotate_token": strconv.FormatBool(config.AutoRotateToken),
"auto_rotate_before": config.AutoRotateBefore.String(),
"base_url": config.BaseURL,
"revoke_auto_rotated_token": strconv.FormatBool(config.RevokeAutoRotatedToken),
})

b.SetClient(nil)
b.Logger().Debug("Wrote new config", "base_url", config.BaseURL, "max_ttl", config.MaxTTL)
b.Logger().Debug("Wrote new config", "base_url", config.BaseURL)
return &logical.Response{
Data: config.LogicalResponseData(),
Warnings: warnings,
Expand Down
Loading

0 comments on commit ccf9a51

Please sign in to comment.