feat: CI/CD pipeline setup complete - Ansible playbooks updated, secrets configured, workflow ready
This commit is contained in:
312
deployment/ansible/README.md
Normal file
312
deployment/ansible/README.md
Normal file
@@ -0,0 +1,312 @@
|
||||
# Ansible Deployment Configuration
|
||||
|
||||
This directory contains Ansible playbooks and configuration for deploying the Custom PHP Framework to production.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
deployment/ansible/
|
||||
├── ansible.cfg # Ansible configuration
|
||||
├── inventory/
|
||||
│ └── production.yml # Production server inventory
|
||||
├── playbooks/
|
||||
│ ├── setup-production-secrets.yml # Deploy secrets
|
||||
│ ├── deploy-update.yml # Deploy application updates
|
||||
│ └── rollback.yml # Rollback deployments
|
||||
├── secrets/
|
||||
│ ├── .gitignore # Prevent committing secrets
|
||||
│ └── production.vault.yml.example # Example vault file
|
||||
└── templates/
|
||||
└── .env.production.j2 # Environment file template
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **Ansible Installed**:
|
||||
```bash
|
||||
pip install ansible
|
||||
```
|
||||
|
||||
2. **SSH Access**:
|
||||
- SSH key configured at `~/.ssh/production`
|
||||
- Key added to production server's authorized_keys for `deploy` user
|
||||
|
||||
3. **Ansible Vault Password**:
|
||||
- Create `.vault_pass` file in `secrets/` directory
|
||||
- Add vault password to this file (one line)
|
||||
- File is gitignored for security
|
||||
|
||||
## Setup Instructions
|
||||
|
||||
### 1. Create Production Secrets
|
||||
|
||||
```bash
|
||||
cd deployment/ansible/secrets
|
||||
|
||||
# Copy example file
|
||||
cp production.vault.yml.example production.vault.yml
|
||||
|
||||
# Edit with your actual secrets
|
||||
nano production.vault.yml
|
||||
|
||||
# Encrypt the file
|
||||
ansible-vault encrypt production.vault.yml
|
||||
# Enter vault password when prompted
|
||||
```
|
||||
|
||||
### 2. Store Vault Password
|
||||
|
||||
```bash
|
||||
# Create vault password file
|
||||
echo "your-vault-password-here" > secrets/.vault_pass
|
||||
|
||||
# Secure the file
|
||||
chmod 600 secrets/.vault_pass
|
||||
```
|
||||
|
||||
### 3. Configure SSH Key
|
||||
|
||||
```bash
|
||||
# Generate SSH key if needed
|
||||
ssh-keygen -t ed25519 -f ~/.ssh/production -C "ansible-deploy"
|
||||
|
||||
# Copy public key to production server
|
||||
ssh-copy-id -i ~/.ssh/production.pub deploy@94.16.110.151
|
||||
```
|
||||
|
||||
## Running Playbooks
|
||||
|
||||
### Deploy Production Secrets
|
||||
|
||||
**First-time setup** - Deploy secrets to production server:
|
||||
|
||||
```bash
|
||||
ansible-playbook playbooks/setup-production-secrets.yml \
|
||||
--vault-password-file secrets/.vault_pass
|
||||
```
|
||||
|
||||
### Deploy Application Update
|
||||
|
||||
**Automated via Gitea Actions** - Or run manually:
|
||||
|
||||
```bash
|
||||
ansible-playbook playbooks/deploy-update.yml \
|
||||
-e "image_tag=sha-abc123" \
|
||||
-e "git_commit_sha=abc123" \
|
||||
-e "deployment_timestamp=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
||||
-e "docker_registry_username=gitea-user" \
|
||||
-e "docker_registry_password=your-registry-password"
|
||||
```
|
||||
|
||||
### Rollback Deployment
|
||||
|
||||
**Rollback to previous version**:
|
||||
|
||||
```bash
|
||||
ansible-playbook playbooks/rollback.yml
|
||||
```
|
||||
|
||||
**Rollback to specific version**:
|
||||
|
||||
```bash
|
||||
ansible-playbook playbooks/rollback.yml \
|
||||
-e "rollback_to_version=2025-01-28T15-30-00"
|
||||
```
|
||||
|
||||
## Ansible Vault Operations
|
||||
|
||||
### View Encrypted File
|
||||
|
||||
```bash
|
||||
ansible-vault view secrets/production.vault.yml \
|
||||
--vault-password-file secrets/.vault_pass
|
||||
```
|
||||
|
||||
### Edit Encrypted File
|
||||
|
||||
```bash
|
||||
ansible-vault edit secrets/production.vault.yml \
|
||||
--vault-password-file secrets/.vault_pass
|
||||
```
|
||||
|
||||
### Change Vault Password
|
||||
|
||||
```bash
|
||||
ansible-vault rekey secrets/production.vault.yml \
|
||||
--vault-password-file secrets/.vault_pass
|
||||
```
|
||||
|
||||
### Decrypt File (Temporarily)
|
||||
|
||||
```bash
|
||||
ansible-vault decrypt secrets/production.vault.yml \
|
||||
--vault-password-file secrets/.vault_pass
|
||||
|
||||
# DO NOT COMMIT DECRYPTED FILE!
|
||||
|
||||
# Re-encrypt when done
|
||||
ansible-vault encrypt secrets/production.vault.yml \
|
||||
--vault-password-file secrets/.vault_pass
|
||||
```
|
||||
|
||||
## Testing Playbooks
|
||||
|
||||
### Test with Check Mode (Dry Run)
|
||||
|
||||
```bash
|
||||
ansible-playbook playbooks/deploy-update.yml \
|
||||
--check \
|
||||
-e "image_tag=test"
|
||||
```
|
||||
|
||||
### Test Connection
|
||||
|
||||
```bash
|
||||
ansible production -m ping
|
||||
```
|
||||
|
||||
### Verify Inventory
|
||||
|
||||
```bash
|
||||
ansible-inventory --list -y
|
||||
```
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
1. **Never commit unencrypted secrets**
|
||||
- `production.vault.yml` must be encrypted
|
||||
- `.vault_pass` is gitignored
|
||||
- Use `.example` files for documentation
|
||||
|
||||
2. **Rotate secrets regularly**
|
||||
- Update vault file
|
||||
- Re-run `setup-production-secrets.yml`
|
||||
- Restart affected services
|
||||
|
||||
3. **Limit SSH key access**
|
||||
- Use separate SSH key for Ansible
|
||||
- Limit key to `deploy` user only
|
||||
- Consider IP restrictions
|
||||
|
||||
4. **Vault password security**
|
||||
- Store vault password in secure password manager
|
||||
- Don't share via insecure channels
|
||||
- Use different passwords for dev/staging/prod
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Vault Decryption Failed
|
||||
|
||||
**Error**: `Decryption failed (no vault secrets were found)`
|
||||
|
||||
**Solution**:
|
||||
```bash
|
||||
# Verify vault password is correct
|
||||
ansible-vault view secrets/production.vault.yml \
|
||||
--vault-password-file secrets/.vault_pass
|
||||
|
||||
# If password is wrong, you'll need the original password to decrypt
|
||||
```
|
||||
|
||||
### SSH Connection Failed
|
||||
|
||||
**Error**: `Failed to connect to the host`
|
||||
|
||||
**Solutions**:
|
||||
```bash
|
||||
# Test SSH connection manually
|
||||
ssh -i ~/.ssh/production deploy@94.16.110.151
|
||||
|
||||
# Check SSH key permissions
|
||||
chmod 600 ~/.ssh/production
|
||||
chmod 644 ~/.ssh/production.pub
|
||||
|
||||
# Verify SSH key is added to server
|
||||
ssh-copy-id -i ~/.ssh/production.pub deploy@94.16.110.151
|
||||
```
|
||||
|
||||
### Docker Registry Authentication Failed
|
||||
|
||||
**Error**: `unauthorized: authentication required`
|
||||
|
||||
**Solution**:
|
||||
```bash
|
||||
# Verify registry credentials in vault file
|
||||
ansible-vault view secrets/production.vault.yml \
|
||||
--vault-password-file secrets/.vault_pass
|
||||
|
||||
# Test registry login manually on production server
|
||||
docker login git.michaelschiemer.de:5000
|
||||
```
|
||||
|
||||
### Service Not Starting
|
||||
|
||||
**Check service logs**:
|
||||
```bash
|
||||
# SSH to production server
|
||||
ssh -i ~/.ssh/production deploy@94.16.110.151
|
||||
|
||||
# Check Docker service logs
|
||||
docker service logs app_app
|
||||
|
||||
# Check stack status
|
||||
docker stack ps app
|
||||
```
|
||||
|
||||
## CI/CD Integration
|
||||
|
||||
These playbooks are automatically executed by Gitea Actions workflows:
|
||||
|
||||
- **`.gitea/workflows/production-deploy.yml`** - Calls `deploy-update.yml` on push to main
|
||||
- **`.gitea/workflows/update-production-secrets.yml`** - Calls `setup-production-secrets.yml` on manual trigger
|
||||
|
||||
Vault password is stored as Gitea Actions secret: `ANSIBLE_VAULT_PASSWORD`
|
||||
|
||||
## Inventory Variables
|
||||
|
||||
All deployment variables are defined in `inventory/production.yml`:
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `docker_registry` | Docker registry URL | git.michaelschiemer.de:5000 |
|
||||
| `app_name` | Application name | framework |
|
||||
| `app_domain` | Production domain | michaelschiemer.de |
|
||||
| `stack_name` | Docker stack name | app |
|
||||
| `compose_file` | Docker Compose file path | /home/deploy/docker-compose.prod.yml |
|
||||
| `secrets_path` | Secrets directory | /home/deploy/secrets |
|
||||
| `backups_path` | Backups directory | /home/deploy/backups |
|
||||
| `max_rollback_versions` | Backup retention | 5 |
|
||||
| `health_check_url` | Health check endpoint | https://michaelschiemer.de/health |
|
||||
|
||||
## Backup Management
|
||||
|
||||
Backups are automatically created before each deployment:
|
||||
|
||||
- **Location**: `/home/deploy/backups/`
|
||||
- **Retention**: Last 5 versions kept
|
||||
- **Contents**:
|
||||
- `current_image.txt` - Previously deployed image
|
||||
- `stack_status.txt` - Stack status before deployment
|
||||
- `deployment_metadata.txt` - Deployment details
|
||||
|
||||
### List Available Backups
|
||||
|
||||
```bash
|
||||
ssh -i ~/.ssh/production deploy@94.16.110.151 \
|
||||
"ls -lh /home/deploy/backups/"
|
||||
```
|
||||
|
||||
### Manual Backup
|
||||
|
||||
```bash
|
||||
ansible-playbook playbooks/deploy-update.yml \
|
||||
--tags backup \
|
||||
-e "image_tag=current"
|
||||
```
|
||||
|
||||
## Support
|
||||
|
||||
For issues with:
|
||||
- **Playbooks**: Check this README and playbook comments
|
||||
- **Vault**: See Ansible Vault documentation
|
||||
- **Deployment**: Review Gitea Actions logs
|
||||
- **Production**: SSH to server and check Docker logs
|
||||
15
deployment/ansible/ansible.cfg
Normal file
15
deployment/ansible/ansible.cfg
Normal file
@@ -0,0 +1,15 @@
|
||||
[defaults]
|
||||
inventory = inventory/production.yml
|
||||
host_key_checking = False
|
||||
remote_user = deploy
|
||||
private_key_file = ~/.ssh/production
|
||||
timeout = 30
|
||||
retry_files_enabled = False
|
||||
gathering = smart
|
||||
fact_caching = jsonfile
|
||||
fact_caching_connection = /tmp/ansible_facts
|
||||
fact_caching_timeout = 3600
|
||||
|
||||
[ssh_connection]
|
||||
pipelining = True
|
||||
control_path = /tmp/ansible-ssh-%%h-%%p-%%r
|
||||
40
deployment/ansible/inventory/production.yml
Normal file
40
deployment/ansible/inventory/production.yml
Normal file
@@ -0,0 +1,40 @@
|
||||
---
|
||||
all:
|
||||
hosts:
|
||||
production:
|
||||
ansible_host: 94.16.110.151
|
||||
ansible_user: deploy
|
||||
ansible_python_interpreter: /usr/bin/python3
|
||||
ansible_ssh_private_key_file: ~/.ssh/production
|
||||
|
||||
vars:
|
||||
# Docker Registry
|
||||
docker_registry: git.michaelschiemer.de:5000
|
||||
docker_registry_url: git.michaelschiemer.de:5000
|
||||
# Registry credentials (can be overridden via -e or vault)
|
||||
docker_registry_username: "{{ docker_registry_username | default('admin') }}"
|
||||
docker_registry_password: "{{ docker_registry_password | default('registry-secure-password-2025') }}"
|
||||
|
||||
# Application Configuration
|
||||
app_name: framework
|
||||
app_domain: michaelschiemer.de
|
||||
app_image: "{{ docker_registry }}/{{ app_name }}"
|
||||
|
||||
# Docker Stack
|
||||
stack_name: app
|
||||
compose_file: /home/deploy/docker-compose.prod.yml
|
||||
|
||||
# Deployment Paths
|
||||
deploy_user_home: /home/deploy
|
||||
app_base_path: "{{ deploy_user_home }}/app"
|
||||
secrets_path: "{{ deploy_user_home }}/secrets"
|
||||
backups_path: "{{ deploy_user_home }}/backups"
|
||||
|
||||
# Health Check
|
||||
health_check_url: "https://{{ app_domain }}/health"
|
||||
health_check_retries: 10
|
||||
health_check_delay: 10
|
||||
|
||||
# Rollback Configuration
|
||||
max_rollback_versions: 5
|
||||
rollback_timeout: 300
|
||||
187
deployment/ansible/scripts/init-secrets.sh
Executable file
187
deployment/ansible/scripts/init-secrets.sh
Executable file
@@ -0,0 +1,187 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Initialize Ansible Secrets
|
||||
# This script helps set up the Ansible vault file for the first time
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ANSIBLE_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
SECRETS_DIR="$ANSIBLE_DIR/secrets"
|
||||
|
||||
echo "🔐 Ansible Secrets Initialization"
|
||||
echo "=================================="
|
||||
echo ""
|
||||
|
||||
# Check if running from correct directory
|
||||
if [ ! -f "$ANSIBLE_DIR/ansible.cfg" ]; then
|
||||
echo "❌ Error: Must run from deployment/ansible directory"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Step 1: Create vault password file
|
||||
echo "Step 1: Vault Password"
|
||||
echo "----------------------"
|
||||
|
||||
if [ -f "$SECRETS_DIR/.vault_pass" ]; then
|
||||
echo "⚠️ Vault password file already exists: $SECRETS_DIR/.vault_pass"
|
||||
read -p "Do you want to replace it? (y/N): " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo "Keeping existing vault password file."
|
||||
else
|
||||
rm "$SECRETS_DIR/.vault_pass"
|
||||
read -sp "Enter new vault password: " VAULT_PASS
|
||||
echo
|
||||
read -sp "Confirm vault password: " VAULT_PASS_CONFIRM
|
||||
echo
|
||||
|
||||
if [ "$VAULT_PASS" != "$VAULT_PASS_CONFIRM" ]; then
|
||||
echo "❌ Passwords don't match!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "$VAULT_PASS" > "$SECRETS_DIR/.vault_pass"
|
||||
chmod 600 "$SECRETS_DIR/.vault_pass"
|
||||
echo "✅ Vault password file created"
|
||||
fi
|
||||
else
|
||||
read -sp "Enter vault password: " VAULT_PASS
|
||||
echo
|
||||
read -sp "Confirm vault password: " VAULT_PASS_CONFIRM
|
||||
echo
|
||||
|
||||
if [ "$VAULT_PASS" != "$VAULT_PASS_CONFIRM" ]; then
|
||||
echo "❌ Passwords don't match!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "$VAULT_PASS" > "$SECRETS_DIR/.vault_pass"
|
||||
chmod 600 "$SECRETS_DIR/.vault_pass"
|
||||
echo "✅ Vault password file created"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# Step 2: Create production vault file
|
||||
echo "Step 2: Production Vault File"
|
||||
echo "-----------------------------"
|
||||
|
||||
if [ -f "$SECRETS_DIR/production.vault.yml" ]; then
|
||||
echo "⚠️ Production vault file already exists"
|
||||
read -p "Do you want to decrypt and edit it? (y/N): " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
ansible-vault edit "$SECRETS_DIR/production.vault.yml" \
|
||||
--vault-password-file "$SECRETS_DIR/.vault_pass"
|
||||
echo "✅ Vault file updated"
|
||||
fi
|
||||
else
|
||||
echo "Creating new vault file from example..."
|
||||
cp "$SECRETS_DIR/production.vault.yml.example" "$SECRETS_DIR/production.vault.yml"
|
||||
|
||||
echo ""
|
||||
echo "⚠️ IMPORTANT: You must edit the vault file and replace all 'change-me' values!"
|
||||
echo ""
|
||||
read -p "Press ENTER to edit the vault file now..."
|
||||
|
||||
${EDITOR:-nano} "$SECRETS_DIR/production.vault.yml"
|
||||
|
||||
echo ""
|
||||
echo "Encrypting vault file..."
|
||||
ansible-vault encrypt "$SECRETS_DIR/production.vault.yml" \
|
||||
--vault-password-file "$SECRETS_DIR/.vault_pass"
|
||||
|
||||
echo "✅ Production vault file created and encrypted"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# Step 3: Verify vault file
|
||||
echo "Step 3: Verification"
|
||||
echo "-------------------"
|
||||
|
||||
echo "Testing vault decryption..."
|
||||
if ansible-vault view "$SECRETS_DIR/production.vault.yml" \
|
||||
--vault-password-file "$SECRETS_DIR/.vault_pass" > /dev/null 2>&1; then
|
||||
echo "✅ Vault file can be decrypted successfully"
|
||||
else
|
||||
echo "❌ Failed to decrypt vault file!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check for example values
|
||||
echo "Checking for unchanged example values..."
|
||||
EXAMPLE_VALUES=$(ansible-vault view "$SECRETS_DIR/production.vault.yml" \
|
||||
--vault-password-file "$SECRETS_DIR/.vault_pass" | grep -c "change-me" || true)
|
||||
|
||||
if [ "$EXAMPLE_VALUES" -gt 0 ]; then
|
||||
echo "⚠️ WARNING: Found $EXAMPLE_VALUES 'change-me' placeholder values!"
|
||||
echo " You must replace these before deploying to production."
|
||||
echo ""
|
||||
read -p "Do you want to edit the vault file now? (y/N): " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
ansible-vault edit "$SECRETS_DIR/production.vault.yml" \
|
||||
--vault-password-file "$SECRETS_DIR/.vault_pass"
|
||||
fi
|
||||
else
|
||||
echo "✅ No placeholder values found"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# Step 4: Setup SSH key
|
||||
echo "Step 4: SSH Key Setup"
|
||||
echo "--------------------"
|
||||
|
||||
SSH_KEY="$HOME/.ssh/production"
|
||||
|
||||
if [ -f "$SSH_KEY" ]; then
|
||||
echo "✅ SSH key already exists: $SSH_KEY"
|
||||
else
|
||||
echo "SSH key not found. Creating new key..."
|
||||
ssh-keygen -t ed25519 -f "$SSH_KEY" -C "ansible-deploy" -N ""
|
||||
chmod 600 "$SSH_KEY"
|
||||
chmod 644 "$SSH_KEY.pub"
|
||||
echo "✅ SSH key created"
|
||||
echo ""
|
||||
echo "📋 Public key:"
|
||||
cat "$SSH_KEY.pub"
|
||||
echo ""
|
||||
echo "⚠️ You must add this public key to the production server:"
|
||||
echo " ssh-copy-id -i $SSH_KEY.pub deploy@94.16.110.151"
|
||||
echo ""
|
||||
read -p "Press ENTER to continue..."
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# Step 5: Test connection
|
||||
echo "Step 5: Connection Test"
|
||||
echo "----------------------"
|
||||
|
||||
read -p "Do you want to test the connection to production? (y/N): " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo "Testing Ansible connection..."
|
||||
if ansible production -m ping 2>&1 | grep -q "SUCCESS"; then
|
||||
echo "✅ Connection successful!"
|
||||
else
|
||||
echo "❌ Connection failed!"
|
||||
echo ""
|
||||
echo "Troubleshooting steps:"
|
||||
echo "1. Verify SSH key is added to server: ssh-copy-id -i $SSH_KEY.pub deploy@94.16.110.151"
|
||||
echo "2. Test SSH manually: ssh -i $SSH_KEY deploy@94.16.110.151"
|
||||
echo "3. Check inventory file: cat $ANSIBLE_DIR/inventory/production.yml"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "✅ Setup complete!"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo "1. Review vault file: ansible-vault view secrets/production.vault.yml --vault-password-file secrets/.vault_pass"
|
||||
echo "2. Deploy secrets: ansible-playbook playbooks/setup-production-secrets.yml --vault-password-file secrets/.vault_pass"
|
||||
echo "3. Deploy application: See README.md for deployment instructions"
|
||||
echo ""
|
||||
echo "📖 For more information, see: deployment/ansible/README.md"
|
||||
6
deployment/ansible/secrets/.gitignore
vendored
Normal file
6
deployment/ansible/secrets/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
# Ansible Vault Files
|
||||
*.vault.yml
|
||||
.vault_pass
|
||||
|
||||
# Decrypted secrets (if any)
|
||||
*.decrypted.yml
|
||||
26
deployment/ansible/secrets/production.vault.yml.example
Normal file
26
deployment/ansible/secrets/production.vault.yml.example
Normal file
@@ -0,0 +1,26 @@
|
||||
---
|
||||
# Ansible Vault Example
|
||||
# Copy this file to production.vault.yml and encrypt with:
|
||||
# ansible-vault encrypt production.vault.yml
|
||||
|
||||
# Database Credentials
|
||||
vault_db_password: "change-me-secure-db-password"
|
||||
vault_db_root_password: "change-me-secure-root-password"
|
||||
|
||||
# Redis Credentials
|
||||
vault_redis_password: "change-me-secure-redis-password"
|
||||
|
||||
# Application Secrets
|
||||
vault_app_key: "change-me-base64-encoded-32-byte-key"
|
||||
vault_jwt_secret: "change-me-jwt-signing-secret"
|
||||
|
||||
# Mail Configuration
|
||||
vault_mail_password: "change-me-mail-password"
|
||||
|
||||
# Docker Registry Credentials
|
||||
vault_docker_registry_username: "gitea-user"
|
||||
vault_docker_registry_password: "change-me-registry-password"
|
||||
|
||||
# Optional: Additional Secrets
|
||||
vault_encryption_key: "change-me-encryption-key"
|
||||
vault_session_secret: "change-me-session-secret"
|
||||
Reference in New Issue
Block a user