Security Policy
Supported Versions
We release patches for security vulnerabilities in the following versions:
Note: As this project is in early development (pre-1.0), we recommend always using the latest version.
Reporting a Vulnerability
We take the security of the UniFi MCP Server seriously. If you believe you have found a security vulnerability, please report it to us as described below.Please DO NOT
- Open a public GitHub issue for security vulnerabilities
- Disclose the vulnerability publicly before it has been addressed
- Exploit the vulnerability beyond the minimum necessary to demonstrate the issue
Please DO
Report security vulnerabilities via GitHub Security Advisories:- Go to the repository’s Security tab
- Click “Report a vulnerability”
- Fill out the vulnerability report form with as much detail as possible
- Use GitHub Security Advisories to report vulnerabilities
What to Include in Your Report
Please include the following information in your report:- Description: A clear description of the vulnerability
- Impact: What an attacker could achieve by exploiting this vulnerability
- Reproduction Steps: Step-by-step instructions to reproduce the vulnerability
- Affected Versions: Which versions of the project are affected
- Suggested Fix: If you have suggestions for how to fix the vulnerability, please include them
- Your Contact Information: So we can follow up with questions if needed
Example Report
Response Timeline
We aim to respond to security vulnerability reports according to the following timeline:- Initial Response: Within 48 hours of receiving the report
- Vulnerability Assessment: Within 7 days, we will provide an assessment of the vulnerability
- Fix Development: Critical vulnerabilities will be addressed within 30 days
- Public Disclosure: After a fix is released and users have had time to update (typically 7-14 days)
Security Best Practices for Contributors
Credential Management
NEVER commit credentials or secrets to the repository:- UniFi API keys (primary authentication method)
- Passwords
- Private keys
- Certificates
- OAuth tokens
- Session tokens
- Encryption keys
- Database connection strings
- Any
.envfiles containing secrets
UniFi API Key Security
Obtaining API Keys Securely:- Log in to UniFi Site Manager
- Navigate to Settings → Control Plane → Integrations
- Create API Key with a descriptive name
- Copy the key immediately - it’s only shown once
- Store in a password manager or secure secret management system
- Rotate API keys every 90 days or sooner
- Immediately rotate if a key is exposed or compromised
- Use separate keys for development, staging, and production
- Revoke old keys after rotation
- Document key rotation procedures in your runbooks
- Environment variables (
.envfile, never committed) - AWS Secrets Manager
- HashiCorp Vault
- Azure Key Vault
- Google Cloud Secret Manager
- Password managers (1Password, LastPass, etc.)
Input Validation
Always validate and sanitize user inputs:API Security
Authentication:- Always use HTTPS for API communications (required for UniFi Cloud API)
- Use API key authentication via
X-API-Keyheader - No session management needed (stateless authentication)
- Store API keys securely using environment variables or secret management
- Respect UniFi API rate limits (100 req/min EA, 10k req/min v1)
- Implement client-side rate limiting
- Add exponential backoff for 429 (rate limit) errors
- Cache frequently accessed data to reduce API calls
- Never expose API keys in error messages
- Log errors securely without exposing secrets
- Provide user-friendly error messages
Dependency Security
Regular Updates:- Keep all dependencies up to date
- Review security advisories for dependencies
- Use automated dependency scanning tools
AI-Specific Security Considerations
When using AI coding assistants:- Review All AI-Generated Code: Never merge AI-generated code without human review
- Validate Security-Critical Code: Extra scrutiny for authentication, authorization, and data handling
- Test Generated Code: Ensure comprehensive test coverage for AI contributions
- Audit AI Permissions: Limit AI assistant access to only necessary resources
- Monitor AI Changes: Track and review all AI-contributed changes in version control
AI_GIT_PRACTICES.md and AGENTS.md for additional AI security guidelines.
Docker Security
Image Security:- Use official base images from trusted sources
- Keep base images updated
- Run containers as non-root users
- Scan images for vulnerabilities
Security Features
Current Security Features
- API Key Authentication: Stateless authentication via official UniFi Cloud API
- Environment-based Configuration: API keys stored in environment variables, not in code
- Secret Masking: Pydantic SecretStr automatically masks sensitive values
- Type Safety: Pydantic models enforce data validation
- Async Security: Non-blocking I/O prevents certain timing attacks
- HTTPS-Only: Secure communication with UniFi Cloud API (TLS 1.2+)
- Pre-commit Hooks: Automated secret detection before commits using detect-secrets
- Dependency Scanning: Automated vulnerability scanning with Safety and Bandit
- Container Security: Docker images scanned with Trivy
- Input Validation: All user inputs validated with Pydantic models
-
Audit Log Encryption: Optional Fernet at-rest encryption of audit payload fields (
UNIFI_AUDIT_LOG_KEY), with non-destructive key rotation -
Wrapper-Level Audit: Every mutating tool call is recorded by the tool wrapper (an
attemptrecord before the controller is contacted, then the outcome), along with denied calls and server start, naming the caller and target controller. If the attempt record cannot be written, the call is refused (UNIFI_AUDIT_FAIL_CLOSED, default on). WithUNIFI_AUDIT_READS=true, read calls get the same two records (principal, tool, arguments, outcome; never the response), so a read-only deployment can see what a token read -
Encrypted Controller Credentials: With the Postgres fleet registry, controller and cloud-account API keys are Fernet-encrypted with
UNIFI_FLEET_CREDENTIAL_KEYbefore they reach the database; the key never does. The server refuses to start without it, a key that cannot decrypt a credential skips that controller instead of failing the fleet, androtate_fleet_credentialsre-encrypts under a new key -
Role-Based Access Control: With the fleet registry, callers use server-issued API tokens (only SHA-256 hashes stored, 90-day default lifetime) with a role (
viewer,operator,admin,fleet-admin) that grants tool risk tiers, optionally narrowed by tool-module patterns and controller labels. Enforcement happens in the tool wrapper before any controller is contacted, and every refusal is audited. Revocation takes effect within 30 seconds; a database error refuses the token.MCP_AUTH_TOKENand stdio remain break-glassfleet-admincallers, flagged in audit records. With or without the registry, anMCP_AUTH_TOKENentry written<role>:<token>(for exampleviewer:<token>) gets that role and the same tier enforcement -
Append-Only Postgres Audit Store: With DATABASE_URL, audit records go to
audit_log, where a trigger refuses UPDATE, DELETE and TRUNCATE for every role, and each server’s hash chain is kept with its head inaudit_chain_heads.verify_audit_chainandsearch_audit_logarefleet-admintools, and every query is audited -
Tamper-Evident Audit Log: Records are hash-chained, HMAC-keyed with
UNIFI_AUDIT_CHAIN_KEY;python -m src.utils.audit_verifydetects edited, deleted, inserted and reordered records, and with an anchored head (--head, or--anchorsfor the SIEM’s anchor events) also records removed from the end.verify_audit_chainchecks each Postgres chain against itsaudit_chain_headsentry (a chain with records but no head is reported too), so deleting the newest records, a whole chain or the whole table is caught, and checks every searchable column (operation, result, principal, controller, time, seq, hash) against the sealed record, so a record cannot be hidden fromsearch_audit_logby editing its columns. A drill (tests/unit/fleet/test_drill_tamper.py) makes each of these changes as the database superuser with triggers disabled and expects every one to be caught -
Debug Passthrough Behind
DEBUG:debug_api_request(raw GET or DELETE to any controller endpoint) is registered only whenDEBUGis truthy. It goes through the same tool wrapper as every other tool:UNIFI_READ_ONLYremoves it,UNIFI_DRY_RUNpreviews it, every call needs thedestructivetier and is audited, and a DELETE needsconfirm=true. The endpoint must be a controller-relative path; absolute and scheme-relative URLs are refused so the API key never leaves for another host. LeaveDEBUGunset in production
Phase 4 Mutating Tools Safety Mechanisms
All mutating tools (Phase 4) implement comprehensive safety features:-
Confirmation Requirement:
-
Dry Run Mode:
-
Audit Logging:
- Every mutating call, every refused call and server start are recorded by
the tool wrapper in
UNIFI_AUDIT_LOG_PATH(defaultaudit.log), or in Postgres with the fleet registry - Each record names the principal, tool, tier, controller, site, the credential-redacted arguments and the outcome
- Format: JSON lines, hash-chained; file created mode 0600 (owner-readable only)
- Reads are not recorded unless refused. Set
UNIFI_AUDIT_READS=trueto record them too: anattemptand an outcome record per read, with the arguments but never the response, so client lists and device inventory stay out of the audit store. Without it, a read-only deployment writes no record of what a token read, and a leaked bearer can pull the whole inventory unseen.UNIFI_AUDIT_FAIL_CLOSEDthen applies to reads as well. Expect two records per read; size rotation and retention for it - Controller and HTTP error text is redacted before it is stored:
credential-named values (
"x_passphrase": "...",?apikey=...,X-API-KEY: ...),Bearertokens anduser:password@in URLs become***. Identifiers such as the MAC inapi.err.MacUsedstay readable, as they do inparameters. The match is by key name, so a secret the controller quotes without its key name is not caught; setUNIFI_AUDIT_LOG_KEYas well if error text can carry one
Use the MCP audit log as evidence of who did what. The A2A file holds tool results and is not redacted, so protect it like the controller data it contains, or keep/a2a/*unexposed. The application log is for operating the server and carries a one-lineAUDIT:summary per record, which is no substitute for the record itself. - Every mutating call, every refused call and server start are recorded by
the tool wrapper in
-
Audit Log Encryption (issue #22):
Audit records contain operation parameters and controller error text that
can carry sensitive identifiers (MAC addresses, IPs, client names, site
UUIDs) and, despite credential redaction, benefit from confidentiality at
rest. Operators can enable at-rest encryption of the sensitive payload
fields by setting
UNIFI_AUDIT_LOG_KEY:What is encrypted vs. what stays readable:Guarantees and behavior:- Backward compatible. With no key configured, the on-disk format is byte-identical to historical plaintext records. Encryption only activates when a key is present.
- Redaction first, encryption second.
sanitize_credentials()runs before encryption, so a decrypted entry is exactly the redacted record an operator would see today. Encryption complements redaction; it never replaces or weakens it. - Non-destructive rotation.
MultiFernetsemantics: the newest key writes, all listed keys are tried on read. Retiring a key never destroys the historical trail. Pre-existing plaintext entries stay readable after enabling encryption. - Fail closed on misconfiguration. A configured-but-unusable key
raises
AuditEncryptionErrorat logger creation instead of silently writing plaintext the operator believed was encrypted. - Key hygiene. The key is never written to the audit file, never logged, and never included in exception messages (errors name the environment variable only). Store it in a secrets manager; never commit it.
- Undecryptable entries are reported, not dropped. A record that no
configured key can decrypt (e.g. after key loss) surfaces as a
<undecryptable: field>placeholder with a logged warning; the rest of the trail remains intact.
Key shape: a 44-character urlsafe-base64Fernet.generate_key()value is used directly; any other string is treated as a passphrase and stretched with PBKDF2-HMAC-SHA256 (600k iterations, fixed application salt) — use high-entropy passphrases.UNIFI_AUDIT_ENCRYPTION_KEYis accepted as a legacy alias when the primary variable is unset. Tamper evidence: every record carrieschain_id,seq,prev_hashandhash, an HMAC-SHA256 over the stored record keyed byUNIFI_AUDIT_CHAIN_KEY(plain SHA-256 when unset). Each server process writes its own chain. The hash covers the stored, possibly encrypted, record, so verifying needs the chain key but not the payload key:Removing the last records of a chain is not detectable from the file alone. Pass a head recorded elsewhere and the chain must end exactly there:In Postgres,verify_audit_chaincompares every chain with itsaudit_chain_headsentry. Someone with write access to the database could rewrite the records and that table together; only a head kept off the server catches that, which is what the SIEM anchors below are for. Use a different value from the payload key and keep it in your secret manager, never in the repository. SIEM export and anchors: setUNIFI_AUDIT_SIEM_TARGETSto copy every stored record (the encrypted-payload form, exactly as written) to one or more destinations. Targets requireUNIFI_AUDIT_CHAIN_KEY, and the server refuses to start without it: an unkeyed SHA-256 chain can be recomputed by whoever edits the records, so the copy and the anchors the SIEM keeps would prove nothing.Delivery never blocks a tool call. Records wait in an on-disk buffer per target (directory 0700, files 0600,UNIFI_AUDIT_SIEM_BUFFER_DIR, defaultsiem-buffernext to the audit log) and are retried with backoff up to 60 s. The buffer survives a restart; pastUNIFI_AUDIT_SIEM_BUFFER_MAX_MBthe oldest records are dropped and counted in a warning. Delivery is at least once, so deduplicate onevent_id. Verify targets withUNIFI_AUDIT_SIEM_CA_FILEfor a private CA. Records are only exported once stored, so a record the server refused to write is never sent. EveryUNIFI_AUDIT_SIEM_ANCHOR_SECONDS(default 300) and on shutdown, the server also sends ananchorevent per chain that moved:chain_id,seqandhashof its latest record. Anchors are tracked per target, so a target that missed one (a failed write, or its buffer dropping it over the cap) gets it again at the next interval. Export the anchors from the SIEM as JSON lines (or a JSON array) and check a log against them (a file with no anchor events is an error, not a pass):Records rewritten together with their heads in Postgres no longer match the anchor the SIEM holds, and the drill intest_drill_tamper.pyshows it. Records written after the last anchor are covered by the next one, so keep the interval short enough for your needs. Retention: withUNIFI_AUDIT_RETENTION_DAYSset, run the retention job as the schema owner (it usesMIGRATION_DATABASE_URLwhen set), for example daily from cron or a Kubernetes CronJob:It refuses to delete anything unless the whole store verifies. Expired records go to a gzip archive inUNIFI_AUDIT_ARCHIVE_DIR(defaultaudit-archivenext to the audit log; directory 0700, files 0600) with a manifest, both written to disk before the delete commits. Move archives to storage of their own; check one with:audit_archivesrecords where each chain was cut, soverify_audit_chainkeeps checking what remains and reports archive rows that were removed. The append-only trigger allows a DELETE only by the table’s owner inside the retention transaction; the server itself never deletes audit records. The run’saudit_retentionadmin event is written in the same transaction as the delete, the job needs no controller credentials (noUNIFI_API_KEY), andUNIFI_READ_ONLYrefuses--confirm. JSONL rotation and retention: without Postgres, setUNIFI_AUDIT_LOG_ROTATE_MBand the server renames the full audit file toaudit.log.<UTC time>Zand starts a new one; the hash chain continues across files, so verify them together, oldest first ([0-9]*matches only the rotated files, not the checkpoint file retention adds). Rotation renames the file, so it must live in a writable directory: in Docker, mount a directory and pointUNIFI_AUDIT_LOG_PATHinto it rather than mounting the file itself:Retention removes rotated files whose records are all pastUNIFI_AUDIT_RETENTION_DAYS(never the file being written), after the same verify-then-archive steps, and records where each chain was cut inaudit.log.checkpoints.jsonl:Whoever can rewrite the log can rewrite the checkpoint file too, so keep the archives (or the SIEM copy) on storage the server cannot change. Least-privilege database role (optional): the server needs no DDL and no UPDATE/DELETE on the audit table. Run migrations as the schema owner (MIGRATION_DATABASE_URL) and pointDATABASE_URLat a role granted only what it uses:The Ansible playbook (deploy/ansible/) creates this role and these grants when it runs Postgres itself. The append-only trigger then backs up the missing grants rather than being the only control. -
Input Validation:
- All parameters validated before execution
- Type checking via Pydantic models
- Custom validators for MAC addresses, IPs, VLANs, etc.
- Raises
ValidationErrorfor invalid inputs
- Firewall Management:
create_firewall_rule,update_firewall_rule,delete_firewall_rule - Network Configuration:
create_network,update_network,delete_network - Device Control:
restart_device,locate_device,upgrade_device - Client Management:
block_client,unblock_client,reconnect_client
Planned Security Enhancements
- Audit logging for all mutating operations (✅ Phase 4)
- At-rest encryption of audit-log payload fields, key rotation via MultiFernet (✅ issue #22)
- Confirmation requirements for dangerous operations (✅ Phase 4)
- Dry-run mode for safe testing (✅ Phase 4)
- Role-based access control (RBAC) for MCP tools
- API key rotation automation
- Request signing for additional security
- IP allowlist/denylist support
- Webhook signature verification
- Enhanced monitoring and alerting for suspicious activity
- Security headers for HTTP responses
- Support for multiple API keys with different scopes
Security Audit History
Compliance and Standards
This project strives to follow:- OWASP Top 10: Web application security best practices
- CWE Top 25: Common weakness enumeration
- NIST Guidelines: Cybersecurity framework recommendations
- Secure Coding Standards: For Python development
Security Contacts
For security-related questions or concerns:- Security Reports: Use GitHub Security Advisories
- Project Issues: GitHub Issues
Acknowledgments
We appreciate the security research community and recognize contributors who responsibly disclose vulnerabilities:Additional Resources
- OWASP Python Security
- Python Security Best Practices
- FastAPI Security
- GitHub Security Best Practices
Last Updated: 2026-03-17 Thank you for helping keep UniFi MCP Server and its users safe!
