Initial commit
This commit is contained in:
693
skills/gitlab/references/authentication.md
Normal file
693
skills/gitlab/references/authentication.md
Normal file
@@ -0,0 +1,693 @@
|
||||
# GitLab Authentication Reference
|
||||
|
||||
## Overview
|
||||
|
||||
GitLab supports multiple authentication methods for accessing the API, Git repositories, and the web interface.
|
||||
|
||||
## Authentication Methods
|
||||
|
||||
### 1. Personal Access Tokens (PATs)
|
||||
|
||||
Most common method for API and Git authentication.
|
||||
|
||||
#### Creating a Personal Access Token
|
||||
|
||||
**Via UI**:
|
||||
1. Navigate to User Settings > Access Tokens
|
||||
2. Enter token name
|
||||
3. Set expiration date (optional but recommended)
|
||||
4. Select scopes
|
||||
5. Click "Create personal access token"
|
||||
6. Copy token (shown only once)
|
||||
|
||||
**Scopes**:
|
||||
- `api` - Complete API access
|
||||
- `read_api` - Read-only API access
|
||||
- `read_user` - Read user information
|
||||
- `read_repository` - Read repository (pull code)
|
||||
- `write_repository` - Write repository (push code)
|
||||
- `read_registry` - Read container registry
|
||||
- `write_registry` - Write container registry
|
||||
- `sudo` - Perform actions as any user (admin only)
|
||||
- `admin_mode` - Admin mode access
|
||||
|
||||
#### Using Personal Access Tokens
|
||||
|
||||
**API requests**:
|
||||
```bash
|
||||
# Header method (preferred)
|
||||
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.com/api/v4/projects"
|
||||
|
||||
# Query parameter method
|
||||
curl "https://gitlab.com/api/v4/projects?private_token=<your_access_token>"
|
||||
```
|
||||
|
||||
**Git operations**:
|
||||
```bash
|
||||
# Clone with token
|
||||
git clone https://oauth2:<your_access_token>@gitlab.com/username/project.git
|
||||
|
||||
# Or configure credential helper
|
||||
git config --global credential.helper store
|
||||
# Then use token as password when prompted
|
||||
```
|
||||
|
||||
**Python example**:
|
||||
```python
|
||||
import requests
|
||||
|
||||
token = "your_access_token"
|
||||
headers = {"PRIVATE-TOKEN": token}
|
||||
|
||||
response = requests.get(
|
||||
"https://gitlab.com/api/v4/projects",
|
||||
headers=headers
|
||||
)
|
||||
print(response.json())
|
||||
```
|
||||
|
||||
#### Token Best Practices
|
||||
|
||||
- Set expiration dates on all tokens
|
||||
- Use minimal required scopes
|
||||
- Store tokens securely (environment variables, secret managers)
|
||||
- Rotate tokens regularly
|
||||
- Revoke unused tokens
|
||||
- Never commit tokens to repositories
|
||||
|
||||
### 2. OAuth 2.0
|
||||
|
||||
OAuth 2.0 for third-party application authorization.
|
||||
|
||||
#### OAuth Application Setup
|
||||
|
||||
**Create OAuth Application**:
|
||||
1. Navigate to User Settings > Applications
|
||||
2. Enter application name
|
||||
3. Set redirect URI
|
||||
4. Select scopes
|
||||
5. Click "Save application"
|
||||
6. Note Application ID and Secret
|
||||
|
||||
**Authorization Code Flow**:
|
||||
|
||||
```python
|
||||
import requests
|
||||
from flask import Flask, request, redirect
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
CLIENT_ID = "your_client_id"
|
||||
CLIENT_SECRET = "your_client_secret"
|
||||
REDIRECT_URI = "http://localhost:5000/callback"
|
||||
GITLAB_URL = "https://gitlab.com"
|
||||
|
||||
@app.route('/login')
|
||||
def login():
|
||||
"""Redirect user to GitLab authorization page"""
|
||||
auth_url = (
|
||||
f"{GITLAB_URL}/oauth/authorize?"
|
||||
f"client_id={CLIENT_ID}&"
|
||||
f"redirect_uri={REDIRECT_URI}&"
|
||||
f"response_type=code&"
|
||||
f"scope=read_user+api"
|
||||
)
|
||||
return redirect(auth_url)
|
||||
|
||||
@app.route('/callback')
|
||||
def callback():
|
||||
"""Handle OAuth callback"""
|
||||
code = request.args.get('code')
|
||||
|
||||
# Exchange code for access token
|
||||
token_response = requests.post(
|
||||
f"{GITLAB_URL}/oauth/token",
|
||||
data={
|
||||
'client_id': CLIENT_ID,
|
||||
'client_secret': CLIENT_SECRET,
|
||||
'code': code,
|
||||
'grant_type': 'authorization_code',
|
||||
'redirect_uri': REDIRECT_URI
|
||||
}
|
||||
)
|
||||
|
||||
token_data = token_response.json()
|
||||
access_token = token_data['access_token']
|
||||
refresh_token = token_data['refresh_token']
|
||||
|
||||
# Use access token for API requests
|
||||
headers = {'Authorization': f'Bearer {access_token}'}
|
||||
user_response = requests.get(
|
||||
f"{GITLAB_URL}/api/v4/user",
|
||||
headers=headers
|
||||
)
|
||||
|
||||
return user_response.json()
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(port=5000)
|
||||
```
|
||||
|
||||
**Refreshing Access Token**:
|
||||
```python
|
||||
def refresh_access_token(refresh_token):
|
||||
response = requests.post(
|
||||
f"{GITLAB_URL}/oauth/token",
|
||||
data={
|
||||
'client_id': CLIENT_ID,
|
||||
'client_secret': CLIENT_SECRET,
|
||||
'refresh_token': refresh_token,
|
||||
'grant_type': 'refresh_token',
|
||||
'redirect_uri': REDIRECT_URI
|
||||
}
|
||||
)
|
||||
return response.json()
|
||||
```
|
||||
|
||||
#### OAuth Scopes
|
||||
|
||||
- `api` - Full API access
|
||||
- `read_user` - Read user information
|
||||
- `read_api` - Read-only API access
|
||||
- `read_repository` - Read repositories
|
||||
- `write_repository` - Write to repositories
|
||||
- `read_registry` - Read container registry
|
||||
- `write_registry` - Write container registry
|
||||
- `sudo` - Admin impersonation (admin only)
|
||||
- `openid` - OpenID Connect
|
||||
- `profile` - User profile info
|
||||
- `email` - User email
|
||||
|
||||
### 3. SSH Keys
|
||||
|
||||
SSH keys for Git operations.
|
||||
|
||||
#### Generating SSH Keys
|
||||
|
||||
```bash
|
||||
# Generate new SSH key
|
||||
ssh-keygen -t ed25519 -C "your_email@example.com"
|
||||
|
||||
# Or RSA (if ed25519 not supported)
|
||||
ssh-keygen -t rsa -b 4096 -C "your_email@example.com"
|
||||
|
||||
# Start SSH agent
|
||||
eval "$(ssh-agent -s)"
|
||||
|
||||
# Add key to agent
|
||||
ssh-add ~/.ssh/id_ed25519
|
||||
```
|
||||
|
||||
#### Adding SSH Key to GitLab
|
||||
|
||||
**Via UI**:
|
||||
1. Navigate to User Settings > SSH Keys
|
||||
2. Paste public key content (`~/.ssh/id_ed25519.pub`)
|
||||
3. Set title
|
||||
4. Optional: Set expiration date
|
||||
5. Click "Add key"
|
||||
|
||||
**Via API**:
|
||||
```bash
|
||||
curl --request POST --header "PRIVATE-TOKEN: <token>" \
|
||||
--data "title=My SSH Key" \
|
||||
--data "key=$(cat ~/.ssh/id_ed25519.pub)" \
|
||||
"https://gitlab.com/api/v4/user/keys"
|
||||
```
|
||||
|
||||
#### Using SSH Keys
|
||||
|
||||
```bash
|
||||
# Clone with SSH
|
||||
git clone git@gitlab.com:username/project.git
|
||||
|
||||
# Configure Git to use SSH
|
||||
git remote set-url origin git@gitlab.com:username/project.git
|
||||
|
||||
# Test SSH connection
|
||||
ssh -T git@gitlab.com
|
||||
```
|
||||
|
||||
#### SSH Key Best Practices
|
||||
|
||||
- Use Ed25519 keys (more secure, faster)
|
||||
- Set passphrase on private keys
|
||||
- Use separate keys for different purposes
|
||||
- Set expiration dates
|
||||
- Store private keys securely
|
||||
- Never share private keys
|
||||
|
||||
### 4. Deploy Keys
|
||||
|
||||
Read-only or read-write SSH keys for specific projects.
|
||||
|
||||
#### Creating Deploy Keys
|
||||
|
||||
**Via UI**:
|
||||
1. Navigate to Project Settings > Repository > Deploy Keys
|
||||
2. Enter title and key content
|
||||
3. Check "Grant write permissions" if needed
|
||||
4. Click "Add key"
|
||||
|
||||
**Via API**:
|
||||
```bash
|
||||
curl --request POST --header "PRIVATE-TOKEN: <token>" \
|
||||
--data "title=Deploy Key" \
|
||||
--data "key=$(cat ~/.ssh/deploy_key.pub)" \
|
||||
--data "can_push=false" \
|
||||
"https://gitlab.com/api/v4/projects/:id/deploy_keys"
|
||||
```
|
||||
|
||||
#### Deploy Key Use Cases
|
||||
|
||||
- CI/CD pipelines
|
||||
- Deployment scripts
|
||||
- Automated processes
|
||||
- Read-only repository access
|
||||
|
||||
### 5. Deploy Tokens
|
||||
|
||||
Project or group-level tokens for registry and package access.
|
||||
|
||||
#### Creating Deploy Tokens
|
||||
|
||||
**Via UI**:
|
||||
1. Navigate to Project/Group Settings > Repository > Deploy Tokens
|
||||
2. Enter name
|
||||
3. Set expiration date
|
||||
4. Select scopes:
|
||||
- `read_repository` - Clone repositories
|
||||
- `read_registry` - Pull container images
|
||||
- `write_registry` - Push container images
|
||||
- `read_package_registry` - Pull packages
|
||||
- `write_package_registry` - Push packages
|
||||
5. Click "Create deploy token"
|
||||
6. Copy username and token
|
||||
|
||||
**Via API**:
|
||||
```bash
|
||||
curl --request POST --header "PRIVATE-TOKEN: <token>" \
|
||||
--header "Content-Type: application/json" \
|
||||
--data '{
|
||||
"name": "My Deploy Token",
|
||||
"expires_at": "2025-12-31",
|
||||
"scopes": ["read_repository", "read_registry"]
|
||||
}' \
|
||||
"https://gitlab.com/api/v4/projects/:id/deploy_tokens"
|
||||
```
|
||||
|
||||
#### Using Deploy Tokens
|
||||
|
||||
**Clone repository**:
|
||||
```bash
|
||||
git clone https://<deploy-token-username>:<deploy-token>@gitlab.com/group/project.git
|
||||
```
|
||||
|
||||
**Pull Docker image**:
|
||||
```bash
|
||||
docker login -u <deploy-token-username> -p <deploy-token> registry.gitlab.com
|
||||
docker pull registry.gitlab.com/group/project/image:tag
|
||||
```
|
||||
|
||||
**CI/CD**:
|
||||
```yaml
|
||||
docker-build:
|
||||
script:
|
||||
- docker login -u $CI_DEPLOY_USER -p $CI_DEPLOY_PASSWORD $CI_REGISTRY
|
||||
- docker pull $CI_REGISTRY_IMAGE:latest
|
||||
```
|
||||
|
||||
### 6. Job Tokens (CI/CD)
|
||||
|
||||
Temporary tokens for CI/CD job authentication.
|
||||
|
||||
#### Using CI_JOB_TOKEN
|
||||
|
||||
**API requests in CI**:
|
||||
```yaml
|
||||
test-api:
|
||||
script:
|
||||
- |
|
||||
curl --header "JOB-TOKEN: $CI_JOB_TOKEN" \
|
||||
"https://gitlab.com/api/v4/projects/$CI_PROJECT_ID"
|
||||
```
|
||||
|
||||
**Clone other repositories**:
|
||||
```yaml
|
||||
build:
|
||||
script:
|
||||
- git clone https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com/group/project.git
|
||||
```
|
||||
|
||||
**Pull Docker images**:
|
||||
```yaml
|
||||
build:
|
||||
before_script:
|
||||
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
|
||||
script:
|
||||
- docker pull $CI_REGISTRY_IMAGE:latest
|
||||
```
|
||||
|
||||
#### Job Token Scope
|
||||
|
||||
Configure which projects can be accessed:
|
||||
|
||||
1. Navigate to Settings > CI/CD > Token Access
|
||||
2. Add allowed projects
|
||||
3. Enable/disable token access
|
||||
|
||||
### 7. Project Access Tokens
|
||||
|
||||
Project-level tokens for automation.
|
||||
|
||||
#### Creating Project Access Tokens
|
||||
|
||||
**Via UI**:
|
||||
1. Navigate to Project Settings > Access Tokens
|
||||
2. Enter token name
|
||||
3. Set expiration date
|
||||
4. Select role (Guest, Reporter, Developer, Maintainer)
|
||||
5. Select scopes
|
||||
6. Click "Create project access token"
|
||||
|
||||
**Via API**:
|
||||
```bash
|
||||
curl --request POST --header "PRIVATE-TOKEN: <token>" \
|
||||
--header "Content-Type: application/json" \
|
||||
--data '{
|
||||
"name": "Project Token",
|
||||
"scopes": ["api"],
|
||||
"access_level": 40,
|
||||
"expires_at": "2025-12-31"
|
||||
}' \
|
||||
"https://gitlab.com/api/v4/projects/:id/access_tokens"
|
||||
```
|
||||
|
||||
**Access Levels**:
|
||||
- 10: Guest
|
||||
- 20: Reporter
|
||||
- 30: Developer
|
||||
- 40: Maintainer
|
||||
- 50: Owner
|
||||
|
||||
### 8. Group Access Tokens
|
||||
|
||||
Group-level tokens for all group projects.
|
||||
|
||||
#### Creating Group Access Tokens
|
||||
|
||||
**Via UI**:
|
||||
1. Navigate to Group Settings > Access Tokens
|
||||
2. Configure token (similar to project tokens)
|
||||
3. Token applies to all group projects
|
||||
|
||||
**Via API**:
|
||||
```bash
|
||||
curl --request POST --header "PRIVATE-TOKEN: <token>" \
|
||||
--header "Content-Type: application/json" \
|
||||
--data '{
|
||||
"name": "Group Token",
|
||||
"scopes": ["api"],
|
||||
"access_level": 40
|
||||
}' \
|
||||
"https://gitlab.com/api/v4/groups/:id/access_tokens"
|
||||
```
|
||||
|
||||
### 9. LDAP Authentication
|
||||
|
||||
Enterprise edition feature for LDAP/Active Directory integration.
|
||||
|
||||
#### LDAP Configuration
|
||||
|
||||
**`/etc/gitlab/gitlab.rb`**:
|
||||
```ruby
|
||||
gitlab_rails['ldap_enabled'] = true
|
||||
gitlab_rails['ldap_servers'] = YAML.load <<-EOS
|
||||
main:
|
||||
label: 'LDAP'
|
||||
host: 'ldap.example.com'
|
||||
port: 636
|
||||
uid: 'sAMAccountName'
|
||||
encryption: 'simple_tls'
|
||||
verify_certificates: true
|
||||
bind_dn: 'CN=query user,OU=Users,DC=example,DC=com'
|
||||
password: 'password'
|
||||
active_directory: true
|
||||
base: 'OU=Users,DC=example,DC=com'
|
||||
user_filter: '(memberOf=CN=GitLab Users,OU=Groups,DC=example,DC=com)'
|
||||
EOS
|
||||
```
|
||||
|
||||
### 10. SAML Authentication
|
||||
|
||||
Enterprise edition SAML SSO support.
|
||||
|
||||
#### SAML Configuration
|
||||
|
||||
**`/etc/gitlab/gitlab.rb`**:
|
||||
```ruby
|
||||
gitlab_rails['omniauth_enabled'] = true
|
||||
gitlab_rails['omniauth_allow_single_sign_on'] = ['saml']
|
||||
gitlab_rails['omniauth_block_auto_created_users'] = false
|
||||
|
||||
gitlab_rails['omniauth_providers'] = [
|
||||
{
|
||||
name: 'saml',
|
||||
args: {
|
||||
assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback',
|
||||
idp_cert_fingerprint: 'XX:XX:XX...',
|
||||
idp_sso_target_url: 'https://idp.example.com/sso',
|
||||
issuer: 'https://gitlab.example.com',
|
||||
name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent'
|
||||
},
|
||||
label: 'Company SSO'
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
### Token Management
|
||||
|
||||
1. **Minimize Scope**: Use least privilege principle
|
||||
2. **Set Expiration**: Always set token expiration dates
|
||||
3. **Rotate Regularly**: Rotate tokens on schedule
|
||||
4. **Secure Storage**: Use secret managers (Vault, AWS Secrets Manager)
|
||||
5. **Monitor Usage**: Audit token usage regularly
|
||||
6. **Revoke Unused**: Remove tokens no longer needed
|
||||
|
||||
### Secure Token Storage
|
||||
|
||||
**Environment Variables**:
|
||||
```bash
|
||||
export GITLAB_TOKEN="your_token"
|
||||
```
|
||||
|
||||
**Git Credentials**:
|
||||
```bash
|
||||
# Store credentials securely
|
||||
git config --global credential.helper 'cache --timeout=3600'
|
||||
```
|
||||
|
||||
**Python example**:
|
||||
```python
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
GITLAB_TOKEN = os.getenv('GITLAB_TOKEN')
|
||||
```
|
||||
|
||||
**Docker Secrets**:
|
||||
```bash
|
||||
echo "your_token" | docker secret create gitlab_token -
|
||||
```
|
||||
|
||||
### Two-Factor Authentication (2FA)
|
||||
|
||||
#### Enabling 2FA
|
||||
|
||||
1. Navigate to User Settings > Account > Two-Factor Authentication
|
||||
2. Scan QR code with authenticator app
|
||||
3. Enter verification code
|
||||
4. Save recovery codes securely
|
||||
|
||||
#### Using 2FA with Git
|
||||
|
||||
When 2FA is enabled, use:
|
||||
- Personal access tokens instead of passwords
|
||||
- SSH keys for Git operations
|
||||
|
||||
```bash
|
||||
# Use token as password
|
||||
git clone https://oauth2:<your_token>@gitlab.com/username/project.git
|
||||
```
|
||||
|
||||
### IP Allowlisting
|
||||
|
||||
Restrict API access by IP address (Premium/Ultimate).
|
||||
|
||||
**Group/Instance Settings**:
|
||||
1. Navigate to Settings > General > Allowed IP addresses
|
||||
2. Add IP ranges
|
||||
3. Save changes
|
||||
|
||||
## API Authentication Examples
|
||||
|
||||
### Python (requests library)
|
||||
|
||||
```python
|
||||
import requests
|
||||
|
||||
class GitLabClient:
|
||||
def __init__(self, token, base_url="https://gitlab.com"):
|
||||
self.token = token
|
||||
self.base_url = base_url
|
||||
self.headers = {"PRIVATE-TOKEN": token}
|
||||
|
||||
def get(self, endpoint):
|
||||
url = f"{self.base_url}/api/v4/{endpoint}"
|
||||
response = requests.get(url, headers=self.headers)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
def post(self, endpoint, data):
|
||||
url = f"{self.base_url}/api/v4/{endpoint}"
|
||||
response = requests.post(url, headers=self.headers, json=data)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
# Usage
|
||||
client = GitLabClient(token="your_token")
|
||||
projects = client.get("projects")
|
||||
```
|
||||
|
||||
### JavaScript (fetch API)
|
||||
|
||||
```javascript
|
||||
class GitLabClient {
|
||||
constructor(token, baseURL = 'https://gitlab.com') {
|
||||
this.token = token;
|
||||
this.baseURL = baseURL;
|
||||
}
|
||||
|
||||
async get(endpoint) {
|
||||
const response = await fetch(`${this.baseURL}/api/v4/${endpoint}`, {
|
||||
headers: {
|
||||
'PRIVATE-TOKEN': this.token
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
async post(endpoint, data) {
|
||||
const response = await fetch(`${this.baseURL}/api/v4/${endpoint}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'PRIVATE-TOKEN': this.token,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
const client = new GitLabClient('your_token');
|
||||
const projects = await client.get('projects');
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/xanzy/go-gitlab"
|
||||
)
|
||||
|
||||
func main() {
|
||||
git, err := gitlab.NewClient("your_token")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// List projects
|
||||
projects, _, err := git.Projects.ListProjects(&gitlab.ListProjectsOptions{})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for _, project := range projects {
|
||||
fmt.Printf("Project: %s\n", project.Name)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Ruby
|
||||
|
||||
```ruby
|
||||
require 'gitlab'
|
||||
|
||||
# Initialize client
|
||||
Gitlab.configure do |config|
|
||||
config.endpoint = 'https://gitlab.com/api/v4'
|
||||
config.private_token = 'your_token'
|
||||
end
|
||||
|
||||
# List projects
|
||||
projects = Gitlab.projects
|
||||
|
||||
projects.each do |project|
|
||||
puts "Project: #{project.name}"
|
||||
end
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Authentication Issues
|
||||
|
||||
**1. 401 Unauthorized**
|
||||
- Verify token is valid and not expired
|
||||
- Check token has required scopes
|
||||
- Ensure token is included in request headers
|
||||
|
||||
**2. 403 Forbidden**
|
||||
- Check user permissions on resource
|
||||
- Verify token scope includes required access
|
||||
- For protected resources, check branch protection rules
|
||||
|
||||
**3. SSH Connection Failed**
|
||||
- Verify SSH key is added to GitLab
|
||||
- Check SSH agent is running
|
||||
- Test connection: `ssh -T git@gitlab.com`
|
||||
- Verify SSH key permissions (600 for private key)
|
||||
|
||||
**4. Token Not Working with 2FA**
|
||||
- Use personal access token instead of password
|
||||
- Or use SSH keys for Git operations
|
||||
|
||||
**5. Deploy Token Issues**
|
||||
- Verify token hasn't expired
|
||||
- Check token scopes match operation
|
||||
- Ensure token is enabled in project settings
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- Personal Access Tokens: https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html
|
||||
- OAuth 2.0: https://docs.gitlab.com/ee/api/oauth2.html
|
||||
- SSH Keys: https://docs.gitlab.com/ee/user/ssh.html
|
||||
- Deploy Keys: https://docs.gitlab.com/ee/user/project/deploy_keys/
|
||||
- Deploy Tokens: https://docs.gitlab.com/ee/user/project/deploy_tokens/
|
||||
Reference in New Issue
Block a user