Presidio
Overview
Presidio is an open source toolkit for finding and removing personally identifiable information from free text. It runs as two cooperating services. The analyzer locates entities such as names, email addresses, phone numbers, credit card numbers and national identifiers. For each entity it returns the entity type, the exact character offsets and a confidence score. The anonymizer redacts, replaces, masks, hashes or encrypts those spans. Presidio was originally created at Microsoft, is now community-governed under the Data Privacy Stack organisation, and is released under the MIT licence.
The SecureAnyCloud image installs both services into a Python virtual environment at /opt/presidio. It runs each service under gunicorn as an unprivileged presidio system account bound to loopback. A single nginx gateway on TCP 80 fronts them and enforces HTTP Basic authentication on every API path. The image generates a unique API password on the first boot of every VM, and it is backed by 24/7 SecureAnyCloud support.
What you get:
- presidio-analyzer 2.2.363 and presidio-anonymizer 2.2.363, both MIT licensed.
- spaCy 3.8.13 with the en_core_web_lg 3.8.0 language model, MIT licensed and verified against the artifact at build time.
- nginx as an authenticating reverse proxy, which is the only surface reachable from the network.
- A per-VM API password, generated on first boot into a root-only file.
- An unauthenticated
/healthzendpoint for load-balancer probes.
Key facts
Item Value
Platform Ubuntu 24.04 LTS
Default SSH user azureuser
Install root /opt/presidio
Credentials file /root/presidio-credentials.txt
API user admin
Analyzer (loopback) 127.0.0.1:5001
Anonymizer (loopback) 127.0.0.1:5002
Public API TCP 80 via nginx
Prerequisites
- An active Azure subscription with permission to create virtual machines.
- An SSH key pair for the
azureuseraccount. - A network security group that allows inbound TCP 22 and TCP 80 from your address ranges.
- Recommended VM size: Standard_B2ms (2 vCPU, 8 GiB). The analyzer holds the language model in memory, measured at roughly 1 GiB resident. Standard_B2s works for evaluation, but it is a burstable size, and sustained analysis exhausts its CPU credits.
Step 1 - Deploy from the Azure Marketplace
- In the Azure Portal, choose Create a resource and search for Presidio on Ubuntu 24.04.
- Select the SecureAnyCloud offering and choose Create.
- Pick your subscription, resource group and region.
- Set the VM size to Standard_B2ms or larger.
- Set the authentication type to SSH public key and the username to
azureuser. - On the Networking tab, allow inbound SSH (22) and HTTP (80).
- Choose Review + create, and then Create.
Step 2 - Deploy from the Azure CLI
az vm image terms accept --urn sac1647283583153:presidio-ubuntu-24-04:default:latest
az vm create \
--resource-group my-resource-group \
--name my-presidio-vm \
--image sac1647283583153:presidio-ubuntu-24-04:default:latest \
--size Standard_B2ms \
--admin-username azureuser \
--generate-ssh-keys \
--public-ip-sku StandardThen open the API port. Restrict it to your own address range in the network security group:
az vm open-port --resource-group my-resource-group --name my-presidio-vm --port 80Step 3 - Connect to your VM
az vm show --resource-group my-resource-group --name my-presidio-vm -d --query publicIps -o tsvssh azureuser@<vm-ip>Step 4 - Confirm the services are running
Check that the three services are active:
systemctl is-active presidio-analyzer presidio-anonymizer nginxExpected output:
active active activeBoth engines listen on loopback only. nginx is the sole public surface, so you cannot reach the analyzer or the anonymizer directly from the network. Confirm the listeners:
ss -tln | grep -E '127.0.0.1:500[12]|:80 'The unauthenticated health probe is safe to expose to a load balancer, because nginx serves it itself and it touches neither engine:
curl -s -o /dev/null -w 'healthz -> HTTP %{http_code}\n' http://127.0.0.1/healthzStep 5 - Retrieve your API password
Every VM generates its own password on first boot. There is no default login and no shared secret between deployments. Read the password from the credentials file:
sudo cat /root/presidio-credentials.txtOnly root can read the file. It contains PRESIDIO_URL, PRESIDIO_USERNAME and PRESIDIO_PASSWORD. Store the password in your secret manager and treat it as you would any API key.
Step 6 - Confirm authentication is enforced
Every API path requires the credential, so nginx refuses an unauthenticated request:
curl -s -o /dev/null -w 'no credential -> HTTP %{http_code}\n' \
-X POST http://127.0.0.1/analyze \
-H 'Content-Type: application/json' \
-d '{"text":"My name is Jane Doe","language":"en"}'Expected output:
no credential -> HTTP 401With the per-VM password, the same request succeeds:
curl -s -o /dev/null -w 'per VM password -> HTTP %{http_code}\n' \
-u admin:<PRESIDIO_PASSWORD> \
-X POST http://127.0.0.1/analyze \
-H 'Content-Type: application/json' \
-d '{"text":"My name is Jane Doe","language":"en"}'Expected output:
per VM password -> HTTP 200Step 7 - Detect personal data
Post text to /analyze. The response is a JSON array. Each entry gives the entity type, the start and end character offsets and a confidence score. The examples below use the reserved example.com domain and the 555-01xx telephone range reserved for fiction, so no real personal data is involved.
curl -s -u admin:<PRESIDIO_PASSWORD> \
-X POST http://127.0.0.1/analyze \
-H 'Content-Type: application/json' \
-d '{"text":"My name is Jane Doe, email jane.doe@example.com, phone 212-555-0143.","language":"en"}'Expected output:
[{"end":47,"entity_type":"EMAIL_ADDRESS","score":1.0,"start":27},{"end":19,"entity_type":"PERSON","score":0.85,"start":11},{"end":67,"entity_type":"PHONE_NUMBER","score":0.75,"start":55},{"end":34,"entity_type":"URL","score":0.5,"start":27},{"end":47,"entity_type":"URL","score":0.5,"start":36}]The offsets make the result actionable. Slicing the original string with start and end returns exactly the detected value, so you can redact in place without guessing where the entity was. Here start 11 and end 19 select Jane Doe, and 27 to 47 selects the email address.
Note the two lower-scoring URL entries. Presidio's recognisers overlap by design, so the domain part of an email address is also reported as a URL with a confidence of 0.5. This is normal, and the anonymizer resolves the overlapping spans for you. To keep only high-confidence results, pass a threshold:
curl -s -u admin:<PRESIDIO_PASSWORD> \
-X POST http://127.0.0.1/analyze \
-H 'Content-Type: application/json' \
-d '{"text":"My name is Jane Doe, email jane.doe@example.com, phone 212-555-0143.","language":"en","score_threshold":0.6}'To list every entity type this image can detect, run:
curl -s -u admin:<PRESIDIO_PASSWORD> http://127.0.0.1/supportedentitiesStep 8 - Redact personal data
Pass the analyzer results to /anonymize together with the operator you want. The replace operator substitutes a token, and DEFAULT applies it to every entity type.
TEXT='My name is Jane Doe, email jane.doe@example.com, phone 212-555-0143.'
RESULTS=$(curl -s -u admin:<PRESIDIO_PASSWORD> \
-X POST http://127.0.0.1/analyze \
-H 'Content-Type: application/json' \
-d "{\"text\":\"$TEXT\",\"language\":\"en\"}")
curl -s -u admin:<PRESIDIO_PASSWORD> \
-X POST http://127.0.0.1/anonymize \
-H 'Content-Type: application/json' \
-d "{\"text\":\"$TEXT\",\"analyzer_results\":$RESULTS,\"anonymizers\":{\"DEFAULT\":{\"type\":\"replace\",\"new_value\":\"<REDACTED>\"}}}"Expected output:
{"items":[{"end":57,"entity_type":"PHONE_NUMBER","operator":"replace","start":47,"text":"<REDACTED>"},{"end":39,"entity_type":"EMAIL_ADDRESS","operator":"replace","start":29,"text":"<REDACTED>"},{"end":21,"entity_type":"PERSON","operator":"replace","start":11,"text":"<REDACTED>"}],"text":"My name is <REDACTED>, email <REDACTED>, phone <REDACTED>."}The start and end values in items refer to positions in the returned text, not the input, because each replacement changes the length of the string. The anonymizer resolved the overlapping URL spans from the previous step automatically, which leaves three replacements.
Other operators are available. redact removes the span entirely, mask replaces part of it with a repeated character, hash substitutes a digest, and encrypt produces a reversible value that you can later pass to /deanonymize. List the operators with:
curl -s -u admin:<PRESIDIO_PASSWORD> http://127.0.0.1/anonymizersStep 9 - Call the API from your application
Point any existing Presidio client at the VM and supply the credential. The gateway preserves the standard Presidio paths unchanged, so client libraries work without modification. For example, in Python:
import requests BASE = "http://<vm-ip>" AUTH = ("admin", "<PRESIDIO_PASSWORD>") text = "Please contact Jane Doe at jane.doe@example.com" results = requests.post(f"{BASE}/analyze", json={"text": text, "language": "en"}, auth=AUTH, timeout=60).json() clean = requests.post(f"{BASE}/anonymize", json={"text": text, "analyzer_results": results, "anonymizers": {"DEFAULT": {"type": "replace", "new_value": "<REDACTED>"}}}, auth=AUTH, timeout=60).json() print(clean["text"])The language model and its licence
Presidio's own code is MIT licensed, but it cannot detect anything without a natural language model, and models are separately licensed artifacts. This image ships the spaCy model en_core_web_lg 3.8.0, which is MIT licensed. The build verifies that licence against the artifact itself rather than taking it from documentation. It checks both the model metadata and the licence file inside the model package, and the build fails unless both agree.
The full record is on the image:
cat /opt/presidio/MODEL-PROVENANCE.txtRecognisers based on Stanza or transformers models are deliberately not installed, because those pull in models whose licences vary per model and are frequently share-alike or non-commercial.
Log hygiene for submitted text
Presidio's threat model is unusual: callers send exactly the sensitive text they are trying to protect. This image is configured so that submitted text is never written to disk:
- Access logging is disabled on every API path in the nginx gateway, and nginx never logs request bodies in any case.
- Both services run gunicorn with access logging sent to
/dev/null. - The services run at a log level that does not echo analysed text, and Presidio's decision-process tracing, which can include matched text, is left disabled.
To confirm this on your own VM, submit a marked string and search the logs for it:
sudo bash -c ' MARKER="ZZPROBE$(openssl rand -hex 4 | tr "a-f" "A-F")ZZ" PW=$(grep "^PRESIDIO_PASSWORD=" /root/presidio-credentials.txt | cut -d= -f2-) curl -s -o /dev/null -u "admin:$PW" -X POST http://127.0.0.1/analyze \
-H "Content-Type: application/json" \
-d "{\"text\":\"My name is $MARKER Doe\",\"language\":\"en\"}" sleep 2 grep -rlF "$MARKER" /var/log 2>/dev/null || echo "not present in /var/log" journalctl -u presidio-analyzer -u presidio-anonymizer -u nginx --no-pager --since "-5 min" | grep -cF "$MARKER" || true 'Expected output:
not present in /var/log 0The whole probe runs inside a single sudo bash -c for a reason: sudo writes the full argument list of every command it runs into auth.log and the journal. Suppose you generated the marker in your own shell and then ran sudo grep ... ZZPROBE.... That grep would log the marker itself and then find it, and it would report a leak caused entirely by your own search. Generating the marker inside the elevated shell keeps it off every command line. The search above therefore covers all of /var/log with no exclusions, and the result means what it says.
Server components
Component Version Purpose
presidio-analyzer 2.2.363 Detects PII entities and returns their offsets
presidio-anonymizer 2.2.363 Redacts, replaces, masks, hashes or encrypts spans
spaCy 3.8.13 Natural language engine
en_core_web_lg 3.8.0 English language model, MIT licensed
nginx distribution Authenticating reverse proxy on TCP 80
gunicorn 26.0.0 WSGI server for both Presidio services
Filesystem layout
Path Purpose
/opt/presidio/venv Python virtual environment holding Presidio, spaCy and the model
/opt/presidio/app The analyzer and anonymizer service applications
/opt/presidio/MODEL-PROVENANCE.txt Shipped components and the licence of each
/var/lib/presidio Writable state and cache for the services
/root/presidio-credentials.txt Per VM API credentials, root only
/etc/nginx/sac-presidio.htpasswd Hashed API password used by the gateway
Managing the services
sudo systemctl restart presidio-analyzer
sudo systemctl restart presidio-anonymizer
sudo systemctl reload nginxThe analyzer loads the language model at startup, so allow up to a minute after a restart before the first request succeeds. To follow its progress, run:
sudo journalctl -u presidio-analyzer -n 30 --no-pagerChanging the API password
sudo sh -c 'printf "admin:%s\n" "$(openssl passwd -apr1 NEW_PASSWORD)" > /etc/nginx/sac-presidio.htpasswd'
sudo systemctl reload nginxUpdate /root/presidio-credentials.txt to match, so that the record on the VM stays accurate.
Enabling HTTPS
The gateway serves plain HTTP on port 80 so that the image works immediately on a private network. Callers send sensitive text to this API, so terminate TLS before you expose it beyond a trusted network. With a DNS name pointed at the VM, run:
sudo apt-get update && sudo apt-get install -y certbot python3-certbot-nginx
sudo certbot --nginx -d presidio.example.comCertbot rewrites the nginx site to listen on 443 and installs a renewal timer. Afterwards, restrict inbound TCP 80 in your network security group.
Troubleshooting
Symptom Diagnosis
HTTP 401 on every call The password is wrong, or the admin user was omitted. Re read /root/presidio-credentials.txt.
HTTP 502 on /analyze The analyzer is not running or is still loading the model. Check systemctl is-active presidio-analyzer.
/analyze returns [] for text that clearly contains PII The language model failed to load. Check sudo journalctl -u presidio-analyzer for an import error.
HTTP 400 from /anonymize Each analyzer_results entry needs entity_type, start and end.
First request after boot is slow Expected. The model is loaded and warmed at startup, which takes tens of seconds.
Security recommendations
- Restrict inbound TCP 80 and 22 to known address ranges in your network security group.
- Terminate TLS before you send real text to this API over any untrusted network.
- Rotate the API password from its generated value if you share it between systems.
- Keep the VM patched with
sudo apt-get update && sudo apt-get upgrade. - Treat
/root/presidio-credentials.txtas a secret and keep its0600permissions.
On startup
On the first boot of every VM, presidio-firstboot.service does the following. It generates the per-VM API password and writes it to /root/presidio-credentials.txt and to the gateway's password file. It starts both engines and brings up nginx. It then proves a real detection and anonymisation round trip before it marks first boot complete at /var/lib/sac/data/presidio-firstboot.done. If that proof fails, the unit fails, so a VM never reports a healthy first boot with a broken language model.
To inspect what it did, run:
sudo journalctl -u presidio-firstboot --no-pagerThat sentinel file guards the unit, so it runs once and is skipped on later reboots.
Support
SecureAnyCloud provides 24/7 support for this image. Contact sales@secureanycloud.com.
Deploy on Azure
Launch Presidio on Ubuntu 24.04 LTS on Azure with 24/7 support from SecureAnyCloud.
View on Marketplace
Need Help?
Our support team is available 24/7.
sales@secureanycloud.com
Overview
Presidio is an open source toolkit for finding and removing personally identifiable information (PII) from free text. It runs as two cooperating services. The analyzer locates entities such as names, email addresses, phone numbers, credit card numbers and national identifiers. For each entity it returns the entity type, the exact character offsets and a confidence score. The anonymizer redacts, replaces, masks, hashes or encrypts those spans. Presidio was originally created at Microsoft, is now community-governed under the Data Privacy Stack organisation, and is released under the MIT licence.
The SecureAnyCloud image installs both services into a Python virtual environment at /opt/presidio. It runs each service under gunicorn as an unprivileged presidio system account bound to loopback. A single nginx gateway on TCP 80 fronts them and enforces HTTP Basic authentication on every API path. The image generates a unique API password on the first boot of every instance, and it is backed by 24/7 SecureAnyCloud support.
What you get:
- presidio-analyzer and presidio-anonymizer, both MIT licensed.
- spaCy with the en_core_web_lg language model, MIT licensed and verified against the artifact at build time.
- nginx as an authenticating reverse proxy, which is the only surface reachable from the network.
- A per-instance API password, generated on first boot into a root-only file.
- An unauthenticated
/healthzendpoint for load-balancer probes.
Key facts
Item Value
Platform Ubuntu 24.04 LTS
Install root /opt/presidio
Credentials file /root/presidio-credentials.txt
API user admin
Analyzer (loopback) 127.0.0.1:5001
Anonymizer (loopback) 127.0.0.1:5002
Public API TCP 80 via nginx (HTTP Basic auth)
Health probe GET /healthz (unauthenticated)
Connecting to your instance
Open port 80 (HTTP API) and, if you want shell access, port 22 (SSH) in the instance security group. Connect over SSH as the default login user for the OS variant you launched:
OS variant SSH login user Example
Ubuntu 24.04 LTS ubuntu ssh ubuntu@<public-ip>
ssh ubuntu@<public-ip>Retrieve the per-instance API password
Every instance generates its own 24-character API password on first boot and writes it to a root-only file. Read it once and store it somewhere safe:
sudo cat /root/presidio-credentials.txtThe file records the API URL, the username (admin) and the generated password. Nothing about the password is baked into the image, so two instances launched from this AMI get two different passwords.
Confirm the services and the security posture
Both engines bind to loopback only. nginx on port 80 is the single reachable surface, and it requires authentication on every API path except the static /healthz probe. Run the following checks:
systemctl is-active presidio-analyzer presidio-anonymizer nginx ss -tln | grep -E '127.0.0.1:500[12]|:80 ' curl -s -o /dev/null -w 'healthz (no auth) -> HTTP %{http_code}\n' http://127.0.0.1/healthz curl -s -o /dev/null -w 'analyze (no auth) -> HTTP %{http_code}\n' \
-X POST http://127.0.0.1/analyze \
-H 'Content-Type: application/json' -d '{"text":"hello","language":"en"}'/healthz returns 200 without credentials, and /analyze returns 401 without credentials.
Detect PII with the analyzer
POST free text to /analyze with the admin credential. Replace <your-password> with the password from the credentials file. The analyzer returns one object per detected entity, with its type, start and end character offsets and a confidence score. All example data below uses the reserved example.com domain and the 555-01xx range reserved for fiction.
curl -s -u 'admin:<your-password>' \
-X POST http://127.0.0.1/analyze \
-H 'Content-Type: application/json' \
-d '{"text":"My name is Jane Doe, email jane.doe@example.com, card 4095-2609-9393-4932, phone 212-555-0143.","language":"en"}'To list every entity type the analyzer can detect, run:
curl -s -u 'admin:<your-password>' http://127.0.0.1/supportedentitiesRedact PII with the anonymizer
Feed the analyzer's results into /anonymize to remove the detected spans. This example replaces each span with a replacement token. The anonymizer also supports mask, hash, redact and encrypt operators.
RESULTS=$(curl -s -u 'admin:<your-password>' \
-X POST http://127.0.0.1/analyze \
-H 'Content-Type: application/json' \
-d '{"text":"My name is Jane Doe, email jane.doe@example.com.","language":"en"}')
curl -s -u 'admin:<your-password>' \
-X POST http://127.0.0.1/anonymize \
-H 'Content-Type: application/json' \
-d "{\"text\":\"My name is Jane Doe, email jane.doe@example.com.\",\"analyzer_results\":$RESULTS,\"anonymizers\":{\"DEFAULT\":{\"type\":\"replace\",\"new_value\":\"[REDACTED]\"}}}"The returned text has every detected span replaced, so the personal data no longer appears in the output.
Calling the API from another machine
From your own workstation, point any Presidio SDK client or HTTP tool at the instance's public IP over port 80, with the same credentials. The proxy serves the analyzer and anonymizer paths unchanged:
curl -s -u 'admin:<your-password>' \
-X POST http://<public-ip>/analyze \
-H 'Content-Type: application/json' \
-d '{"text":"Contact John Smith at john.smith@example.com","language":"en"}'Only /healthz is reachable without the credential. Every other path returns 401 until you authenticate.
Log hygiene for submitted text
The text you submit is the sensitive data in this product, so the image is configured not to persist it. Access logging is disabled on both services and on every nginx API path. The services run at log level WARNING, with Presidio's decision-process tracing left disabled. To confirm that nothing you submit is written to the journal, run:
curl -s -o /dev/null -u 'admin:<your-password>' \
-X POST http://127.0.0.1/analyze \
-H 'Content-Type: application/json' \
-d '{"text":"audit probe for Alex Roe at alex.roe@example.com","language":"en"}'
sudo journalctl -u presidio-analyzer -u presidio-anonymizer -u nginx --no-pager --since "-5 min" | grep -c 'alex.roe@example.com' || trueThe grep returns 0, which shows that the submitted address does not appear in the logs.
The shipped language model and its licence
Presidio's own code is MIT, but it does nothing without an NLP model, and models are separately licensed artifacts. This image ships the spaCy en_core_web_lg model. The build verifies the model's MIT licence against the installed artifact and records it in a provenance file on the image:
cat /opt/presidio/MODEL-PROVENANCE.txtStanza and transformer-based recognizers are deliberately not installed, because many of those models carry share-alike or research-only licences that do not permit commercial redistribution.
Change the API password
To rotate the password, write a new nginx htpasswd entry and reload nginx:
sudo sh -c 'printf "admin:%s\n" "$(openssl passwd -apr1 <new-password>)" > /etc/nginx/sac-presidio.htpasswd'
sudo systemctl reload nginxEnable HTTPS (recommended for production)
The image serves the API over plain HTTP on port 80. For production, terminate TLS in front of Presidio. Point a DNS record at the instance, open port 443, and install a certificate with certbot:
sudo apt-get update && sudo apt-get install -y certbot python3-certbot-nginx
sudo certbot --nginx -d <your-domain> --agree-tos -m admin@<your-domain> --redirectcertbot rewrites the nginx server block to serve the authenticated API over HTTPS and to redirect HTTP to HTTPS.
Maintenance
- Restart the stack:
sudo systemctl restart presidio-analyzer presidio-anonymizer nginx - View service logs:
sudo journalctl -u presidio-analyzer -n 50 --no-pager - Apply OS updates:
sudo apt-get update && sudo apt-get upgrade -y
The analyzer loads its ~1 GiB language model at start, so allow a minute after a restart before the first request.
Support
24/7 SecureAnyCloud technical support backs this image, via email and live chat. Support covers deployment, instance sizing for CPU NLP workloads, credential management, anonymization operators, model provenance and licensing questions, API integration, and TLS and VPC configuration for production. Critical issues receive a one-hour average response time. Contact sales@secureanycloud.com.
Deploy on AWS
Launch Presidio PII Detection and Anonymization with 24/7 support from SecureAnyCloud.
View on Marketplace
Need Help?
Our support team is available 24/7.
sales@secureanycloud.com