Enclosed 1.16.1
Overview
Enclosed is an open-source service for sending private notes and files. You write a note, such as a password, an API key or a document, and optionally attach files. You choose when it expires, whether it is deleted after the first reading and whether it needs an extra password, then send the link.
The note is encrypted in your browser (AES-256-GCM) with a key generated on your device. The key sits in the part of the link after the #, which browsers never send to a server, so the instance only ever receives and stores ciphertext.
This guide covers the SecureAnyCloud Enclosed 1.16.1 on Ubuntu 24.04 AMI for Amazon EC2, a hardened, ready-to-use Enclosed server. It is one version of the SecureAnyCloud identity, secrets and PKI listing on AWS Marketplace, selected as the software version when you subscribe and launch.
What you get:
- The official Enclosed container, pinned. The image runs the upstream
corentinth/enclosed:1.16.1container image unmodified, pinned by its digest and stored on the AMI, so a new instance never downloads it. Docker CE comes from Docker's own repository with its signing key pinned by fingerprint, and the Docker daemon listens only on its local socket. - nginx in front, Enclosed behind it. nginx is the only public listener (ports 80 and 443). The Enclosed container is published on
127.0.0.1:8787, the loopback address only. - HTTPS from the first boot. Browsers allow the Web Crypto functions Enclosed needs only on a secure (HTTPS) page. The image therefore serves HTTPS from the first boot with a certificate generated on your instance (an end-entity certificate, not a certificate authority). Port 80 redirects to HTTPS. The instance's name is set from its own EC2 public DNS name at first boot, and you can change it to your own domain.
- A private instance with a per-instance login. Creating a note needs a login. First boot creates one account,
admin@enclosed.local, with a random password unique to your instance, and a random JWT signing secret for the login sessions. Anyone you send a link to can open that note without an account, because the key is in the link. The upstream default signing secret (change-me) can never be used: a start-up check refuses to start Enclosed with it, or with any missing, short or misplaced secret. - Secrets kept out of the container configuration. The signing secret and the account list are in a file only root can reach, mounted read-only into the container and read by Node.js itself. They are not shown by
docker inspect, on any command line, in any systemd unit or in a process environment. - A locked-down container. The container runs as a dedicated non-root user with a read-only root filesystem, no Linux capabilities,
no-new-privileges, memory and process limits, and no core dumps. - Abuse limits. nginx allows 10 login attempts per minute per client address (burst 5), 30 new notes per minute (burst 20) and 20 API requests per second. The client address is the real TCP peer, never a header a client can forge. Every note must expire (10 minutes to 30 days, default 1 hour), a note is at most 50 MB once encrypted, and a page on another website cannot log in or create notes here.
- Browser hardening. A Content-Security-Policy allows only Enclosed's own scripts (the one inline configuration script is pinned by its hash), refuses
evaland third-party resources, and refuses framing. The server sends no referrer. The nginx access log records/api/notes/:idinstead of note ids, and Enclosed's own request log is kept out of the system journal. - Locked down. TLS 1.2 and 1.3 only. The container, Docker, containerd, nginx, Enclosed and the first-boot service are all blocked from the instance metadata service, and the AMI requires IMDSv2.
- Fully patched Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on for Ubuntu and for Docker CE.
The image contains the Enclosed 1.16.1 container image (Node.js 22.23.4 on Alpine Linux 3.24.3), Docker CE 29.9.2, nginx from Ubuntu 24.04, cloud-init and the AWS Systems Manager agent.
Default user: ubuntu; Architecture: x86_64; Recommended instance type: m5.large.
Prerequisites
- An AWS account subscribed to the listing, an EC2 key pair and a VPC subnet.
- A security group that allows TCP 22 (SSH) from your own networks only, TCP 443 (the web app) and TCP 80 (redirects to HTTPS). Recipients of a note link must reach port 443 to open it, so open 443 to wherever your recipients are. Creating a note always needs the admin login. Do not open 8787, because the container listens on the loopback address only.
- Enough root volume space. Notes and attachments are stored, encrypted, on the root volume. The default root volume leaves about 3.5 GB free, so choose a larger root volume at launch if you expect to keep many large attachments.
- For production, a DNS name you control (for example
notes.example.com) and, ideally, a certificate from a certificate authority. Until then, the instance uses its EC2 public DNS name with its own self-signed certificate.
Launch the instance
In the AWS Marketplace listing, choose Continue to Subscribe, then Continue to Configuration. Pick Enclosed 1.16.1 on Ubuntu 24.04 as the software version and select your Region. Then choose Launch through EC2.
Choose m5.large, your key pair and a security group as described above.
To launch with the AWS CLI instead, replace the values in angle brackets:
aws ec2 run-instances --image-id <ami-id-from-the-listing> --instance-type m5.large \
--key-name <your-key-pair> --security-group-ids <your-security-group> --subnet-id <your-subnet> \
--metadata-options HttpTokens=required,HttpEndpoint=enabled \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=enclosed}]'First boot takes about half a minute after the instance is running.
To use your own name from the start, put it in /etc/sac/enclosed-domain with user data, for example a cloud-init write_files entry. Otherwise, first boot uses the instance's public DNS name, or its public IP address if it has no DNS name.
Connect to your instance
Operating system SSH user
Ubuntu 24.04 ubuntu
ssh -i <your-key.pem> ubuntu@<public-ip>Check the services
systemctl is-active enclosed-firstboot docker enclosed nginxactive active active activeOnly SSH, HTTP and HTTPS are public. The Enclosed container is published on the loopback address only:
sudo ss -tln | grep -E ':(22|80|443|8787) 'LISTEN 0 4096 0.0.0.0:22 0.0.0.0:* LISTEN 0 511 0.0.0.0:80 0.0.0.0:* LISTEN 0 511 0.0.0.0:443 0.0.0.0:* LISTEN 0 4096 127.0.0.1:8787 0.0.0.0:* LISTEN 0 4096 [::]:22 [::]:* LISTEN 0 511 [::]:80 [::]:* LISTEN 0 511 [::]:443 [::]:*Run the local health check. It is answered only on the loopback address:
curl -s http://127.0.0.1/api/ping; echo{"status":"ok"}Read the address, admin login and certificate fingerprint
sudo cat /root/enclosed-credentials.txtThe file (root-only, mode 0600) holds this instance's address, the admin email and password, and the SHA-256 fingerprint of its certificate:
# Enclosed 1.16.1 - written on first boot by enclosed-firstboot.service. Unique to this instance. ENCLOSED_URL=https://ec2-32-201-68-126.compute-1.amazonaws.com/ ENCLOSED_ADMIN_EMAIL=admin@enclosed.local ENCLOSED_ADMIN_PASSWORD=<per-instance password> ENCLOSED_TLS_SHA256=6F:F0:1F:70:...:(64 hex digits in pairs)
# Log in at ENCLOSED_URL with the e-mail and password above to CREATE notes. Anyone you send a note link to can
# open it WITHOUT an account: the decryption key travels in the link's #fragment, which never reaches this server. ...The certificate is self-signed and unique to your instance, so your browser warns about it. Open the certificate details in the browser and check that its SHA-256 fingerprint matches ENCLOSED_TLS_SHA256. Only then continue.
Do not add the certificate to your operating system's trust store. For production, replace it with a certificate from a certificate authority (Step 9). The certificate is an end-entity certificate for this instance's names only:
openssl x509 -in /etc/ssl/enclosed/enclosed.crt -noout -subject -ext basicConstraints,keyUsage,extendedKeyUsage,subjectAltNamesubject=CN = ec2-32-201-68-126.compute-1.amazonaws.com, O = sac X509v3 Subject Alternative Name: DNS:ec2-32-201-68-126.compute-1.amazonaws.com, IP Address:32.201.68.126, IP Address:172.31.94.230 X509v3 Basic Constraints: critical CA:FALSE X509v3 Key Usage: critical Digital Signature, Key Encipherment X509v3 Extended Key Usage: TLS Web Server AuthenticationSend a note
Browse to the ENCLOSED_URL address. A visitor who is not logged in is sent to the login page, because on this private instance only an account can create notes. Log in with ENCLOSED_ADMIN_EMAIL and ENCLOSED_ADMIN_PASSWORD.
Write the note. You can add a note password (the recipient must type it, and it is not part of the link), choose the expiration delay, switch on delete after reading and attach files.
Choose Create note. Your browser encrypts the note and its files and uploads only the ciphertext. The page then shows the share link, also as a QR code. Send the link to the recipient over any channel, and send the note password over a different one.
Open a note (no account needed)
The recipient opens the link in any browser, without logging in. A note that is deleted after reading says so first, and it is fetched only when the recipient chooses to see it.
A password-protected note then asks for its password. A wrong password does not decrypt it. With the right password, the browser decrypts the note and its attachments.
The note is now gone from the server, and opening the same link again shows that it was not found.
A note that is not deleted after reading can be opened until it expires. Expired notes are refused and deleted.
Use the API
The web app talks to a small JSON API, which you can also call from scripts. The Enclosed command-line client, npx @enclosed/cli, encrypts for you.
The server never sees a key. Anything you send as payload is stored as you send it, so encrypt it first. Creating a note needs a login token, and reading a public note does not.
Run the commands below on the instance itself. They use the instance's own certificate as the trust anchor and keep the password and token in a private temporary directory rather than on a command line:
U=$(sudo sed -n 's/^ENCLOSED_URL=//p' /root/enclosed-credentials.txt) CA=/etc/ssl/enclosed/enclosed.crt NOTE='{"payload":"already-encrypted-text","deleteAfterReading":true,"ttlInSeconds":3600,"encryptionAlgorithm":"aes-256-gcm","serializationFormat":"cbor-array"}' curl -s --cacert "$CA" -o /dev/null -w 'anonymous create: %{http_code}\n' -H 'Content-Type: application/json' --data "$NOTE" "${U}api/notes" umask 077;
T=$(mktemp -d) sudo sed -n 's/^ENCLOSED_ADMIN_PASSWORD=//p' /root/enclosed-credentials.txt | python3 -c 'import json,sys; print(json.dumps({"email": "admin@enclosed.local", "password": sys.stdin.read().strip()}))' > "$T/login.json" curl -s --cacert "$CA" -H 'Content-Type: application/json' --data @"$T/login.json" "${U}api/auth/login" | python3 -c 'import json,sys; print("Authorization: Bearer " + json.load(sys.stdin)["accessToken"])' > "$T/auth"
ID=$(curl -s --cacert "$CA" -H @"$T/auth" -H 'Content-Type: application/json' --data "$NOTE" "${U}api/notes" | python3 -c 'import json,sys; print(json.load(sys.stdin)["noteId"])')
echo "created a note with a ${#ID}-character id" curl -s --cacert "$CA" -o /dev/null -w 'first read: %{http_code}\n' "${U}api/notes/${ID}" curl -s --cacert "$CA" -o /dev/null -w 'second read: %{http_code}\n' "${U}api/notes/${ID}" rm -rf "$T"anonymous create: 401 created a note with a 26-character id first read: 200 second read: 404A request made by a page on another website (its Origin is not this instance) is refused, and so is any cross-origin preflight:
U=$(sudo sed -n 's/^ENCLOSED_URL=//p' /root/enclosed-credentials.txt)
curl -s --cacert /etc/ssl/enclosed/enclosed.crt -o /dev/null -w 'cross-site create: %{http_code}\n' -H 'Origin: https://evil.example' -H 'Content-Type: application/json' --data '{}' "${U}api/notes"
curl -s --cacert /etc/ssl/enclosed/enclosed.crt -o /dev/null -w 'preflight: %{http_code}\n' -X OPTIONS -H 'Origin: https://evil.example' -H 'Access-Control-Request-Method: POST' "${U}api/notes"cross-site create: 403 preflight: 403Manage the admin account and its secrets
First boot generates the admin password and the JWT signing secret on each instance. Check that the login works and that a wrong password does not:
sudo enclosed-admin checkENCLOSED_ADMIN_OK login=200 wrong=401To get a new admin password and a new signing secret, run sudo enclosed-admin rotate. Every logged-in session ends, and links you already sent keep working. The new password is written to /root/enclosed-credentials.txt.
The secrets live in a file only root can reach, and the container reads it through a read-only mount:
sudo stat -c '%a %U:%G %n' /etc/enclosed/secrets /etc/enclosed/secrets/enclosed.env700 root:root /etc/enclosed/secrets 600 enclosed:enclosed /etc/enclosed/secrets/enclosed.envThe container's own configuration holds only non-secret settings:
sudo docker inspect enclosed --format '{{range .Config.Env}}{{println .}}{{end}}' | grep -E '^(PUBLIC_IS_AUTHENTICATION_REQUIRED|NOTES_MAX|AUTHENTICATION_)'PUBLIC_IS_AUTHENTICATION_REQUIRED=true NOTES_MAX_ENCRYPTED_PAYLOAD_LENGTH=52428800 AUTHENTICATION_JWT_DURATION_SECONDS=86400Use your own domain and certificate
Point a DNS record (for example notes.example.com) at the instance, then run:
sudo enclosed-set-domain notes.example.comnginx then serves that name, and the instance's EC2 name and addresses keep working. If the certificate is still the SecureAnyCloud self-signed one, a new self-signed certificate is issued for the new name. Share links use the name the sender browsed to.
To use a certificate from a certificate authority, replace /etc/ssl/enclosed/enclosed.crt (full chain) and /etc/ssl/enclosed/enclosed.key (mode 0600, root), then run sudo systemctl reload nginx. enclosed-set-domain never overwrites a certificate that is not SecureAnyCloud's own.
Check the current name and fingerprint with sudo enclosed-set-domain --show.
Settings, limits and storage
The non-secret settings are in /etc/enclosed/enclosed.env:
grep -E '^(PUBLIC_IS_|PUBLIC_DEFAULT_NOTE_TTL|NOTES_MAX|AUTHENTICATION_JWT_DURATION)' /etc/enclosed/enclosed.envPUBLIC_IS_AUTHENTICATION_REQUIRED=true PUBLIC_IS_SETTING_NO_EXPIRATION_ALLOWED=false PUBLIC_DEFAULT_NOTE_TTL_SECONDS=3600 NOTES_MAX_ENCRYPTED_PAYLOAD_LENGTH=52428800 AUTHENTICATION_JWT_DURATION_SECONDS=86400After editing the file, run sudo systemctl restart enclosed.
This appliance is a private instance. The start-up check refuses to start Enclosed with PUBLIC_IS_AUTHENTICATION_REQUIRED set to anything but true, and it refuses any secret in this file, because the file is visible to docker inspect. PUBLIC_IS_SETTING_NO_EXPIRATION_ALLOWED=false means every note expires.
If you raise NOTES_MAX_ENCRYPTED_PAYLOAD_LENGTH above 50 MB, also raise client_max_body_size (55m, in the location = /api/notes block) in /usr/local/lib/sac/enclosed/nginx-site.tmpl. Then run sudo enclosed-set-domain --boot and sudo systemctl reload nginx.
nginx limits how fast one client address can log in, create notes and call the API. The address is the real TCP peer. A forged X-Forwarded-For header neither changes it nor appears in the log:
sudo grep -E 'limit_req_zone' /etc/nginx/conf.d/sac-enclosed-maps.conflimit_req_zone $enc_post_key zone=enc_login:10m rate=10r/m; limit_req_zone $enc_post_key zone=enc_create:10m rate=30r/m; limit_req_zone $binary_remote_addr zone=enc_api:10m rate=20r/s;A client over a limit gets HTTP 429 until its allowance refills. The access log records the note API without note ids:
sudo tail -2 /var/log/nginx/access.log | sed -E 's/^[0-9.]+ /<client-ip> /'<client-ip> - - [30/Oct/2026:11:37:33 +0000] "POST /api/notes HTTP/2.0" 403 146 "curl/8.5.0" <client-ip> - - [30/Oct/2026:11:37:33 +0000] "OPTIONS /api/notes HTTP/2.0" 403 146 "curl/8.5.0"Storage. Notes (ciphertext only) are files in /var/lib/enclosed/data, owned by the container's user. Expired notes are deleted every hour and whenever someone asks for one. To see how many notes are stored, run:
sudo enclosed-storage countTo delete every stored note, stop Enclosed and purge the store: sudo systemctl stop enclosed && sudo enclosed-storage purge && sudo systemctl start enclosed.
A backup of /var/lib/enclosed/data holds only ciphertext. Without the links, which hold the keys, it cannot be read.
Security notes
- The container. It runs the pinned upstream image as a non-root user, with a read-only root filesystem, no capabilities and
no-new-privileges. It is published on the loopback address only:
sudo docker inspect enclosed --format 'user={{.Config.User}} readonly={{.HostConfig.ReadonlyRootfs}} capdrop={{.HostConfig.CapDrop}} privileged={{.HostConfig.Privileged}} securityopt={{.HostConfig.SecurityOpt}} ports={{json .HostConfig.PortBindings}}'user=997:986 readonly=true capdrop=[ALL] privileged=false securityopt=[no-new-privileges:true] ports={"8787/tcp":[{"HostIp":"127.0.0.1","HostPort":"8787"}]}- Metadata service. The AMI requires IMDSv2. Firewall rules that Docker loads before any container starts deny the metadata addresses to containers, and systemd denies them to Docker, containerd, Enclosed, nginx and the first-boot service. Enclosed makes no outbound requests.
sudo iptables -S DOCKER-USER systemctl show -p IPAddressDeny enclosed docker | grep -c 169.254.169.254-N DOCKER-USER -A DOCKER-USER -d 168.63.129.16/32 -j DROP -A DOCKER-USER -d 169.254.169.254/32 -j DROP 2- Response headers. Every page is served with a Content-Security-Policy,
X-Frame-Options: DENY,X-Content-Type-Options: nosniff,Referrer-Policy: no-referrerand HSTS. API responses are not cacheable. The policy blocks the web fonts Enclosed would otherwise load from a third-party font service, so a recipient's browser never contacts a third party and pages use the browser's own fonts. It also blockseval, and Enclosed's decoder then uses its built-in fallback. - Advisories. The container image's operating system packages have no known vulnerabilities at build time. The Node.js image it is built on also ships the
npmcommand-line tool, whose bundled modules have published advisories.npmis never run on this image: the container runsnodedirectly, as a non-root user with a read-only filesystem. One advisory in Enclosed's web framework (Hono CVE-2026-69207, rated medium: slow parsing of a craftedAccess-Control-Request-Headersheader) is closed on this image, because nginx answers every cross-origin preflight itself and never forwards that header to Enclosed. - Logs. Enclosed logs each request path (note ids, never keys) to its container log. Only root can read that log (
sudo docker logs enclosed), it is removed whenever the container restarts, and it is kept out of the system journal. - Upgrades. Ubuntu security updates (including nginx) and Docker CE updates install automatically. The Enclosed container is pinned by digest and never updated in place. SecureAnyCloud publishes new versions of this AMI for new Enclosed releases.
Licences and source
Enclosed is licensed under the Apache License 2.0. The image ships its licence and the exact source and image references:
cat /usr/share/doc/sac-enclosed/SOURCE.txt head -3 /usr/share/doc/sac-enclosed/LICENSE-enclosed-Apache-2.0.txtsac Enclosed 1.16.1 appliance - source Enclosed (Apache License 2.0, LICENSE-enclosed-Apache-2.0.txt; Copyright the Enclosed authors, Corentin Thomasset): https://github.com/CorentinTh/enclosed/tree/v1.16.1 (commit cb6db13bd486622d48c0d880ecb7bd08023f1f30) This image runs the OFFICIAL upstream container image UNMODIFIED: corentinth/enclosed:1.16.1 OCI index sha256:e65f73b085823ceeab1a39c515614c1a24e9b5c69b8e1221d3d7071a4f8bc4a8 linux/amd64 sha256:fb6547f358c3230e616be3b35f307a0847da55e6ba7ec3d9dc50eeedee993ec5 (the reference this instance runs) image config sha256:275fda1429f7def403ebfecb3c3f58564fb38a786d2e4de748a0302f1ddc7d65 (Node.js 22.23.4 on Alpine Linux 3.24.3; the image's own notices are inside it.) sac adds deployment configuration only: systemd units, an nginx HTTPS reverse proxy, per-instance secrets and container hardening (see /etc/systemd/system/enclosed*.service, /etc/nginx, /etc/enclosed). Docker CE is installed from Docker's official repository (download.docker.com), key pinned by fingerprint. Enclosed is a trademark of its respective owner; sac is not affiliated with or endorsed by the Enclosed project. Apache License Version 2.0, January 2004 http://www.apache.org/licenses/All product and company names are trademarks or registered trademarks of their respective holders. SecureAnyCloud is not affiliated with or endorsed by them.
Support
SecureAnyCloud supports this image. Email sales@secureanycloud.com.
See the identity, secrets and PKI listing guide for the other versions in this listing.
Deploy on AWS
Launch Enclosed 1.16.1 on Ubuntu 24.04 on AWS with 24/7 support from SecureAnyCloud.
View on Marketplace
Need Help?
Our support team is available 24/7.
sales@secureanycloud.com