Implement Phase 3 of Custom DNS Provider Plugin Support with comprehensive
security controls for external plugin loading.
Add CHARON_PLUGIN_SIGNATURES env var for SHA-256 signature allowlisting
Support permissive (unset), strict ({}), and allowlist modes
Add directory permission verification (reject world-writable)
Configure container with non-root user and read-only plugin mount option
Add 22+ security tests for permissions, signatures, and allowlist logic
Create plugin-security.md operator documentation
Security controls:
Signature verification with sha256: prefix requirement
World-writable directory rejection
Non-root container execution (charon user UID 1000)
Read-only mount support for production deployments
Documented TOCTOU mitigation with atomic deployment workflow
349 lines
8.6 KiB
Markdown
349 lines
8.6 KiB
Markdown
# Plugin Security Guide
|
|
|
|
This guide covers security configuration and deployment patterns for Charon's plugin system. For general plugin installation and usage, see [Custom Plugins](./custom-plugins.md).
|
|
|
|
## Overview
|
|
|
|
Charon supports external DNS provider plugins via Go's plugin system. Because plugins execute **in-process** with full memory access, they must be treated as trusted code. This guide explains how to:
|
|
|
|
- Configure signature-based allowlisting
|
|
- Deploy plugins securely in containers
|
|
- Mitigate common attack vectors
|
|
|
|
---
|
|
|
|
## Plugin Signature Allowlisting
|
|
|
|
Charon supports SHA-256 signature verification to ensure only approved plugins are loaded.
|
|
|
|
### Environment Variable
|
|
|
|
```bash
|
|
CHARON_PLUGIN_SIGNATURES='{"pluginname": "sha256:..."}'
|
|
```
|
|
|
|
**Key format**: Plugin name **without** the `.so` extension.
|
|
|
|
### Behavior Matrix
|
|
|
|
| `CHARON_PLUGIN_SIGNATURES` Value | Behavior |
|
|
|----------------------------------|----------|
|
|
| Unset or empty (`""`) | **Permissive mode** — All plugins are loaded (backward compatible) |
|
|
| Set to `{}` | **Strict block-all** — No external plugins are loaded |
|
|
| Set with entries | **Allowlist mode** — Only listed plugins with matching signatures are loaded |
|
|
|
|
### Examples
|
|
|
|
**Permissive mode (default)**:
|
|
```bash
|
|
# Unset — all plugins load without verification
|
|
unset CHARON_PLUGIN_SIGNATURES
|
|
```
|
|
|
|
**Strict block-all**:
|
|
```bash
|
|
# Empty object — no external plugins will load
|
|
export CHARON_PLUGIN_SIGNATURES='{}'
|
|
```
|
|
|
|
**Allowlist specific plugins**:
|
|
```bash
|
|
# Only powerdns and custom-provider plugins are allowed
|
|
export CHARON_PLUGIN_SIGNATURES='{"powerdns": "sha256:a1b2c3d4...", "custom-provider": "sha256:e5f6g7h8..."}'
|
|
```
|
|
|
|
---
|
|
|
|
## Generating Plugin Signatures
|
|
|
|
To add a plugin to your allowlist, compute its SHA-256 signature:
|
|
|
|
```bash
|
|
sha256sum myplugin.so | awk '{print "sha256:" $1}'
|
|
```
|
|
|
|
**Example output**:
|
|
```
|
|
sha256:a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6a7b8c9d0e1f2
|
|
```
|
|
|
|
Use this value in your `CHARON_PLUGIN_SIGNATURES` JSON:
|
|
|
|
```bash
|
|
export CHARON_PLUGIN_SIGNATURES='{"myplugin": "sha256:a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6a7b8c9d0e1f2"}'
|
|
```
|
|
|
|
> **⚠️ Important**: The key is the plugin name **without** `.so`. Use `myplugin`, not `myplugin.so`.
|
|
|
|
---
|
|
|
|
## Container Deployment Recommendations
|
|
|
|
### Read-Only Plugin Mount (Critical)
|
|
|
|
**Always mount the plugin directory as read-only in production**:
|
|
|
|
```yaml
|
|
# docker-compose.yml
|
|
services:
|
|
charon:
|
|
image: charon:latest
|
|
volumes:
|
|
- ./plugins:/app/plugins:ro # Read-only mount
|
|
environment:
|
|
- CHARON_PLUGINS_DIR=/app/plugins
|
|
- CHARON_PLUGIN_SIGNATURES={"powerdns": "sha256:..."}
|
|
```
|
|
|
|
This prevents runtime modification of plugin files, mitigating:
|
|
- Time-of-check to time-of-use (TOCTOU) attacks
|
|
- Malicious plugin replacement after signature verification
|
|
|
|
### Non-Root Execution
|
|
|
|
Run Charon as a non-root user:
|
|
|
|
```yaml
|
|
# docker-compose.yml
|
|
services:
|
|
charon:
|
|
image: charon:latest
|
|
user: "1000:1000" # Non-root user
|
|
# ...
|
|
```
|
|
|
|
Or in Dockerfile:
|
|
```dockerfile
|
|
FROM charon:latest
|
|
USER charon
|
|
```
|
|
|
|
### Directory Permissions
|
|
|
|
Plugin directories must **not** be world-writable. Charon enforces this at startup.
|
|
|
|
| Permission | Result |
|
|
|------------|--------|
|
|
| `0755` or stricter | ✅ Allowed |
|
|
| `0777` (world-writable) | ❌ Rejected — plugin loading disabled |
|
|
|
|
**Set secure permissions**:
|
|
```bash
|
|
chmod 755 /path/to/plugins
|
|
chmod 644 /path/to/plugins/*.so # Or 755 for executable
|
|
```
|
|
|
|
### Complete Secure Deployment Example
|
|
|
|
```yaml
|
|
# docker-compose.production.yml
|
|
services:
|
|
charon:
|
|
image: charon:latest
|
|
user: "1000:1000"
|
|
read_only: true
|
|
security_opt:
|
|
- no-new-privileges:true
|
|
volumes:
|
|
- ./plugins:/app/plugins:ro
|
|
- ./data:/app/data
|
|
environment:
|
|
- CHARON_PLUGINS_DIR=/app/plugins
|
|
- CHARON_PLUGIN_SIGNATURES={"powerdns": "sha256:abc123..."}
|
|
tmpfs:
|
|
- /tmp
|
|
```
|
|
|
|
---
|
|
|
|
## TOCTOU Mitigation
|
|
|
|
Time-of-check to time-of-use (TOCTOU) vulnerabilities occur when a file is modified between signature verification and loading. Mitigate with:
|
|
|
|
### 1. Read-Only Mounts (Primary Defense)
|
|
|
|
Mount the plugin directory as read-only (`:ro`). This prevents modification after startup.
|
|
|
|
### 2. Atomic File Replacement for Updates
|
|
|
|
When updating plugins, use atomic operations to avoid partial writes:
|
|
|
|
```bash
|
|
# 1. Copy new plugin to temporary location
|
|
cp new_plugin.so /tmp/plugin.so.new
|
|
|
|
# 2. Atomically replace the old plugin
|
|
mv /tmp/plugin.so.new /app/plugins/plugin.so
|
|
|
|
# 3. Restart Charon to reload plugins
|
|
docker compose restart charon
|
|
```
|
|
|
|
> **⚠️ Warning**: `cp` followed by direct write to the plugin directory is **not atomic** and creates a window for exploitation.
|
|
|
|
### 3. Signature Re-Verification on Reload
|
|
|
|
After updating plugins, always update your `CHARON_PLUGIN_SIGNATURES` with the new hash before restarting.
|
|
|
|
---
|
|
|
|
## Troubleshooting
|
|
|
|
### Checking if a Plugin Loaded
|
|
|
|
**Check startup logs**:
|
|
```bash
|
|
docker compose logs charon | grep -i plugin
|
|
```
|
|
|
|
**Expected success output**:
|
|
```
|
|
INFO Loaded DNS provider plugin type=powerdns name="PowerDNS" version="1.0.0"
|
|
INFO Loaded 1 external DNS provider plugins (0 failed)
|
|
```
|
|
|
|
**If using allowlist**:
|
|
```
|
|
INFO Plugin signature allowlist enabled with 2 entries
|
|
```
|
|
|
|
**Via API**:
|
|
```bash
|
|
curl http://localhost:8080/api/admin/plugins \
|
|
-H "Authorization: Bearer YOUR-TOKEN"
|
|
```
|
|
|
|
### Common Error Messages
|
|
|
|
#### `plugin not in allowlist`
|
|
|
|
**Cause**: The plugin filename (without `.so`) is not in `CHARON_PLUGIN_SIGNATURES`.
|
|
|
|
**Solution**: Add the plugin to your allowlist:
|
|
```bash
|
|
# Get the signature
|
|
sha256sum powerdns.so | awk '{print "sha256:" $1}'
|
|
|
|
# Add to environment
|
|
export CHARON_PLUGIN_SIGNATURES='{"powerdns": "sha256:YOUR_HASH_HERE"}'
|
|
```
|
|
|
|
#### `signature mismatch for plugin`
|
|
|
|
**Cause**: The plugin file's SHA-256 hash doesn't match the allowlist.
|
|
|
|
**Solution**:
|
|
1. Verify you have the correct plugin file
|
|
2. Re-compute the signature: `sha256sum plugin.so`
|
|
3. Update `CHARON_PLUGIN_SIGNATURES` with the correct hash
|
|
|
|
#### `plugin directory has insecure permissions`
|
|
|
|
**Cause**: The plugin directory is world-writable (mode `0777` or similar).
|
|
|
|
**Solution**:
|
|
```bash
|
|
chmod 755 /path/to/plugins
|
|
chmod 644 /path/to/plugins/*.so
|
|
```
|
|
|
|
#### `invalid CHARON_PLUGIN_SIGNATURES JSON`
|
|
|
|
**Cause**: Malformed JSON in the environment variable.
|
|
|
|
**Solution**: Validate your JSON:
|
|
```bash
|
|
echo '{"powerdns": "sha256:abc123"}' | jq .
|
|
```
|
|
|
|
Common issues:
|
|
- Missing quotes around keys or values
|
|
- Trailing commas
|
|
- Single quotes instead of double quotes
|
|
|
|
#### Permission denied when loading plugin
|
|
|
|
**Cause**: File permissions too restrictive or ownership mismatch.
|
|
|
|
**Solution**:
|
|
```bash
|
|
# Check current permissions
|
|
ls -la /path/to/plugins/
|
|
|
|
# Fix permissions
|
|
chmod 644 /path/to/plugins/*.so
|
|
chown charon:charon /path/to/plugins/*.so
|
|
```
|
|
|
|
### Debugging Checklist
|
|
|
|
1. **Is the plugin directory configured?**
|
|
```bash
|
|
echo $CHARON_PLUGINS_DIR
|
|
```
|
|
|
|
2. **Does the plugin file exist?**
|
|
```bash
|
|
ls -la $CHARON_PLUGINS_DIR/*.so
|
|
```
|
|
|
|
3. **Are directory permissions secure?**
|
|
```bash
|
|
stat -c "%a %n" $CHARON_PLUGINS_DIR
|
|
# Should be 755 or stricter
|
|
```
|
|
|
|
4. **Is the signature correct?**
|
|
```bash
|
|
sha256sum $CHARON_PLUGINS_DIR/myplugin.so
|
|
```
|
|
|
|
5. **Is the JSON valid?**
|
|
```bash
|
|
echo "$CHARON_PLUGIN_SIGNATURES" | jq .
|
|
```
|
|
|
|
---
|
|
|
|
## Security Implications
|
|
|
|
### What Plugins Can Access
|
|
|
|
Plugins run **in-process** with Charon and have access to:
|
|
|
|
| Resource | Access Level |
|
|
|----------|--------------|
|
|
| System memory | Full read/write |
|
|
| Database credentials | Full access |
|
|
| API tokens and secrets | Full access |
|
|
| File system | Charon's permissions |
|
|
| Network | Unrestricted outbound |
|
|
|
|
### Risk Assessment
|
|
|
|
| Risk | Mitigation |
|
|
|------|------------|
|
|
| Malicious plugin code | Signature allowlisting, code review |
|
|
| Plugin replacement attack | Read-only mounts, atomic updates |
|
|
| World-writable directory | Automatic permission verification |
|
|
| Supply chain compromise | Verify plugin source, pin signatures |
|
|
|
|
### Best Practices Summary
|
|
|
|
1. ✅ **Enable signature allowlisting** in production
|
|
2. ✅ **Mount plugin directory read-only** (`:ro`)
|
|
3. ✅ **Run as non-root user**
|
|
4. ✅ **Use strict directory permissions** (`0755` or stricter)
|
|
5. ✅ **Verify plugin source** before deployment
|
|
6. ✅ **Update signatures** after plugin updates
|
|
7. ❌ **Never use permissive mode** in production
|
|
8. ❌ **Never install plugins from untrusted sources**
|
|
|
|
---
|
|
|
|
## See Also
|
|
|
|
- [Custom Plugins](./custom-plugins.md) — Plugin installation and usage
|
|
- [Security Policy](../../SECURITY.md) — Security reporting and policies
|
|
- [Plugin Development Guide](../development/plugin-development.md) — Building custom plugins
|