Pluto.jl 1.1.0
Overview
This guide shows you how to launch and use the Pluto.jl 1.1.0 on Ubuntu 24.04 AMI from SecureAnyCloud on Amazon EC2. The image is one version of the SecureAnyCloud developer and API tools listing on AWS Marketplace. You choose it as the software version when you subscribe and launch.
Pluto.jl is a reactive notebook for the Julia language. When you change a cell, every cell that depends on it runs again on its own, so what you see always matches the code. Notebooks are plain .jl files, and each one carries its own package list. You work in them from any browser while the computation runs on your EC2 instance.
The SecureAnyCloud image runs Pluto as a private notebook server. What you get:
- HTTPS from the first boot. nginx serves Pluto on port 443 with a certificate generated on your instance's first boot. Port 80 only redirects to HTTPS, and a request for any name other than your instance's own gets no answer.
- No default password. The image contains no password, no certificate and no host name. First boot generates them and writes them to
/var/lib/sac/data/pluto-credentials.txt(root only). nginx keeps only a SHA-512 crypt hash of the password, and it asks for the password on every page, file and notebook connection. - Pluto's own secret stays on. Pluto opens and creates notebooks only for a browser that holds its secret cookie (
Secure,HttpOnly,SameSite=Strict). Pluto gives the cookie to the page you signed in to. Another website cannot use your signed-in browser to open a notebook, because cross-site requests and cross-site notebook connections are refused. - Current Julia and Pluto. The image has Julia 1.13.3 (the official release, checked against its published SHA-256 digest) and Pluto 1.1.0. The Manifest pins all 70 of Pluto's dependencies, and they are precompiled into the image. The editor and each new notebook start without compiling Pluto first.
- Notebooks that cannot reach the metadata service. Pluto and every notebook run as their own account,
pluto, with no login shell and nosudo. A notebook cell can reach the internet (to install packages or fetch data) but not the EC2 instance metadata service. Code in a notebook therefore cannot read the instance's IAM role credentials. - Requests limited per address. The limit uses the address nginx accepted the connection from, so a forged
X-Forwarded-Forheader changes nothing. - Fully patched base. Ubuntu 24.04 LTS is fully patched at build time, with unattended security upgrades switched on.
What is on the image: Julia 1.13.3 (the official julia-1.13.3-linux-x86_64.tar.gz), Pluto 1.1.0 and its dependencies in a read-only package depot, nginx, Ubuntu 24.04 LTS, cloud-init and the AWS Systems Manager agent. The build record, Julia's licence and the licence of every package Pluto uses are in /usr/share/doc/sac-pluto/.
Default user: ubuntu. Architecture: x86_64. Recommended instance type: m5.large.
Prerequisites
- An AWS account that is subscribed to the listing, an EC2 key pair and a VPC subnet.
- A security group that allows TCP 443 (notebooks), TCP 80 (redirects to HTTPS) and TCP 22 (SSH). A notebook runs any Julia code you type, so open 443 and 22 only to your own networks (for example
<your-mgmt-cidr>). Avoid opening them to the whole internet. - A modern browser (Chrome, Edge, Firefox or Safari).
Launch the instance
In the AWS Marketplace listing, choose Continue to Subscribe, then Continue to Configuration. Pick Pluto.jl 1.1.0 on Ubuntu 24.04 as the software version, pick your Region, and choose Launch through EC2. Choose m5.large, your key pair, your subnet and the security group from the prerequisites. The m5.large has 8 GiB of memory, which Julia needs because it compiles the packages your notebooks use the first time they load. A t3.medium is enough for light notebooks.
Your notebooks and the packages they install live on the root volume, in /var/lib/pluto. The image's root volume is 16 GiB. Give it more at launch if your notebooks will use large packages or data sets (for example 50 GiB).
To launch with the AWS CLI instead, replace the values in angle brackets:
aws ec2 run-instances --region <region> --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 \
--block-device-mappings '[{"DeviceName":"/dev/sda1","Ebs":{"VolumeSize":50,"VolumeType":"gp3"}}]' \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=pluto}]'Attach an Elastic IP to keep the instance's address, and so its name and certificate, the same across stops and starts.
Connect to your instance
Version SSH user
Pluto.jl 1.1.0 on Ubuntu 24.04 ubuntu
ssh -i <your-key.pem> ubuntu@<public-ip>The login message shows your notebook server's address and where its password is stored.
Check the services
First boot takes under a minute. It sets the server's name, generates the certificate and the password, and only then starts Pluto and nginx. Pluto itself takes a few more seconds to load. Check that everything is running:
systemctl is-active pluto-egress-guard pluto nginx \
&& sudo test -f /var/lib/sac/data/pluto-firstboot.done && echo FIRSTBOOT_DONEactive active active FIRSTBOOT_DONEOnly SSH and nginx listen on the network. Pluto (the julia process) listens on the loopback address 127.0.0.1:1234 behind nginx. While a notebook is open, its own Julia process also listens on a loopback port. Confirm the listeners:
sudo ss -lntp | awk 'NR>1 && $4 !~ /^127\.0\.0\.5[34]/ {print $4, $6}' | sed -E 's/,pid=[0-9]+,fd=[0-9]+//g' | sort -u | tee /dev/stderr \
| awk '$1 !~ /^127\./ {sub(/.*:/, "", $1); print $1}' | sort -un | xargs | grep -qx '22 80 443' && echo "network listeners: 22 80 443"0.0.0.0:22 users:(("sshd"),("systemd")) 0.0.0.0:443 users:(("nginx"),("nginx"),("nginx")) 0.0.0.0:80 users:(("nginx"),("nginx"),("nginx")) 127.0.0.1:1234 users:(("julia")) [::]:22 users:(("sshd"),("systemd")) [::]:443 users:(("nginx"),("nginx"),("nginx")) [::]:80 users:(("nginx"),("nginx"),("nginx")) network listeners: 22 80 443Get your address and password
Run sudo cat /var/lib/sac/data/pluto-credentials.txt to see your notebook server's address, user name and password. Only root can read the file. It holds the following settings, all generated for this instance:
sudo sed -n 's/^\(PLUTO_[A-Z0-9_]*\)=.*/\1/p' /var/lib/sac/data/pluto-credentials.txt | tee /dev/stderr \
| grep -qx PLUTO_PASSWORD && echo "credentials present"PLUTO_URL PLUTO_HOST PLUTO_USERNAME PLUTO_PASSWORD PLUTO_TLS_CERT PLUTO_TLS_SHA256_FINGERPRINT credentials presentPLUTO_URL is https://<your instance's public DNS name>/, PLUTO_USERNAME is admin and PLUTO_PASSWORD is the password. To show only the settings that are not secret, run:
sudo grep -E '^PLUTO_(URL|USERNAME|TLS_SHA256_FINGERPRINT)=' /var/lib/sac/data/pluto-credentials.txt \
| tee /dev/stderr | grep -q '^PLUTO_URL=https://'PLUTO_URL=https://ec2-203-0-113-10.compute-1.amazonaws.com/ PLUTO_USERNAME=admin PLUTO_TLS_SHA256_FINGERPRINT=6C:1A:...:9Enginx stores the password only as a SHA-512 crypt hash, in a file that only root and nginx can read. Verify it:
sudo grep -c '^admin:\$6\$' /etc/nginx/pluto.htpasswd | grep -qx 1 \
&& sudo stat -c '%U:%G %a' /etc/nginx/pluto.htpasswd | tee /dev/stderr | grep -qx 'root:www-data 640' && echo "hash only"root:www-data 640 hash onlySign in from your browser
Open PLUTO_URL in your browser. The certificate is self-signed and was generated on your instance, so the browser warns about it the first time. Before you accept it, check that it is your instance's certificate. The SHA-256 fingerprint in the browser's certificate viewer must match PLUTO_TLS_SHA256_FINGERPRINT. Alternatively, copy the certificate to your machine and compare it there:
scp -i <your-key.pem> ubuntu@<public-ip>:/etc/ssl/pluto/pluto.crt ./pluto.crt openssl x509 -in pluto.crt -noout -fingerprint -sha256To remove the warning for good, add pluto.crt to your machine's trusted certificates (Keychain Access on macOS, certmgr on Windows). You can also use your own domain and a public certificate (Step 9).
The browser then asks for a user name and password. Enter admin and PLUTO_PASSWORD. Pluto's main menu opens. From there you can create a notebook, open one from the notebook folder or try the featured examples:
The browser remembers the password until you close it. To sign out, close every window of the browser, or use a private window for the session.
Work in a notebook
The image puts a short welcome notebook in the notebook folder, /var/lib/pluto/notebooks. In the main menu, type /var/lib/pluto/notebooks/welcome.jl under Open a notebook and choose Open. Pluto opens a notebook file it did not create itself in Safe preview, which shows the code without running it. A notebook from someone else therefore cannot run anything until you have read it. Choose Run notebook code at the top to run it. Change x and watch every value that depends on it update:
Choose Create a new notebook to start an empty one. Pluto saves notebooks to /var/lib/pluto/notebooks. Choose Save notebook... at the top to name it. A notebook's packages are part of the notebook. Write using Plots (or using DataFrames, using PlutoUI and so on) in a cell, and Pluto installs the package for that notebook from the Julia package server. The first time a package is used, Julia downloads and compiles it, which can take a few minutes. After that it loads quickly. Installed packages and their compiled code are kept in /var/lib/pluto/.julia.
To bring in a notebook from elsewhere, use Open a notebook with a URL to a .jl file. You can also copy the file into the notebook folder over SSH (sudo install -o pluto -g pluto -m 0640 my-notebook.jl /var/lib/pluto/notebooks/).
What a notebook can reach
Every notebook runs as the pluto account, inside the Pluto service. It can reach the internet (the Julia package server, data downloads, APIs) but not the EC2 instance metadata service. The screenshot below shows that check run in a notebook cell. The metadata service is refused and a public site answers:
You can run the same check over SSH. The metadata service is refused for the pluto account but still answers for ubuntu (your SSH account). The Julia package server answers for pluto:
{
sudo -u pluto curl -s -m 5 -o /dev/null -w 'pluto -> metadata: %{http_code}\n' -X PUT http://169.254.169.254/latest/api/token -H 'X-aws-ec2-metadata-token-ttl-seconds: 60'
sudo -u pluto curl -sL -m 20 -o /dev/null -w 'pluto -> pkg.julialang.org: %{http_code}\n' https://pkg.julialang.org/registries curl -s -m 5 -o /dev/null -w 'ubuntu -> metadata: %{http_code}\n' -X PUT http://169.254.169.254/latest/api/token -H 'X-aws-ec2-metadata-token-ttl-seconds: 60' } | tee /dev/stderr | xargs | grep -qx 'pluto -> metadata: 000 pluto -> pkg.julialang.org: 200 ubuntu -> metadata: 200' && echo "metadata blocked for notebooks"pluto -> metadata: 000 pluto -> pkg.julialang.org: 200 ubuntu -> metadata: 200 metadata blocked for notebooksTwo mechanisms enforce this: the Pluto service's own network filter (systemd IPAddressDeny) and an nftables rule that rejects the metadata addresses for the pluto account anywhere on the instance. If a notebook needs AWS credentials, give it its own, with only the permissions it needs. For example, store them in a file under /var/lib/pluto that only pluto can read.
A notebook can reach your VPC and other private addresses, like any program on the instance. Pluto itself makes no requests on anyone else's behalf, so the image does not fence private networks.
The pluto account has no sudo and cannot write outside /var/lib/pluto. To install system libraries, use your SSH session as ubuntu (sudo apt-get install ...).
Run the self-test
The image includes a self-test. It checks the following:
- The services and listening ports are as expected.
- A request with no password or a wrong one is refused on every path and on the notebook connection.
- The password in the credentials file opens Pluto over HTTPS with this instance's certificate and sets Pluto's secret cookie (
Secure,HttpOnly,SameSite=Strict). - A new notebook and the notebook connection need that cookie, and nginx refuses a
//newpath. - A notebook connection from another site is refused.
- An uploaded notebook computes.
Run it with:
sudo pluto-selftest | tail -n 1 | tee /dev/stderr | grep -q '^PLUTO_SELFTEST_OK 'PLUTO_SELFTEST_OK no password or a wrong one = 401 everywhere; the note's password opens https://ec2-203-0-113-10.compute-1.amazonaws.com/ (Secure+HttpOnly+SameSite=Strict secret cookie); without the cookie /new and the WebSocket = 403, //new = 400; same-origin WebSocket 101, cross-origin 403; an uploaded notebook computed SAC-SELFTEST-42The self-test uploads, runs and deletes its own small notebook. It does not touch yours.
Change the password or use your own domain
To give the server a new random password, run the command below. The old password stops working at once. Then read the new PLUTO_PASSWORD with sudo cat /var/lib/sac/data/pluto-credentials.txt. Open browser tabs must sign in again, and running notebooks keep running:
sudo pluto-rotate-passwordThe new password goes to the hasher on standard input. Only its SHA-512 crypt hash goes into /etc/nginx/pluto.htpasswd, and the password never appears on a command line or in a log.
To use your own domain, point a DNS name at the instance (an Elastic IP keeps it stable), then make it the server's name. This generates a new certificate for that name and pins nginx to it:
sudo pluto-set-site notebooks.example.comTo use a certificate from a public CA instead, for example one issued by certbot with a DNS challenge, run pluto-set-site first. Then replace /etc/ssl/pluto/pluto.crt (full chain) and /etc/ssl/pluto/pluto.key (mode 0600), and run sudo systemctl reload nginx. Browsers then show no warning.
What the front door refuses
nginx answers only for the server's own names, asks for the password on every path, and passes requests to Pluto on loopback. The command below sends five requests and prints the status of each:
- No password gets
401. - The password gets
200. It is read from the credentials file without putting it on a command line. - A name that is not the server's gets no answer (
000). - Plain HTTP gets
301to HTTPS. - A cross-site form post gets
403, refused before the password is even checked.
H="$(sudo sed -n 's/^PLUTO_HOST=//p' /var/lib/sac/data/pluto-credentials.txt)" C=(--cacert /etc/ssl/pluto/pluto.crt --resolve "$H:443:127.0.0.1") { curl -s -o /dev/null -w '%{http_code}\n' "${C[@]}" "https://$H/" curl -s -o /dev/null -w '%{http_code}\n' "${C[@]}" -K <(sudo sed -n 's/^PLUTO_PASSWORD=\(.*\)$/user = "admin:\1"/p' /var/lib/sac/data/pluto-credentials.txt) "https://$H/" curl -s -o /dev/null -w '%{http_code}\n' -k -H 'Host: someone-else.example' https://127.0.0.1/ curl -s -o /dev/null -w '%{http_code}\n' --resolve "$H:80:127.0.0.1" "http://$H/" curl -s -o /dev/null -w '%{http_code}\n' "${C[@]}" -X POST -H 'Origin: https://evil.example' "https://$H/new" } | xargs | tee /dev/stderr | grep -qx '401 200 000 301 403' && echo "front door OK"401 200 000 301 403 front door OK- Pluto's secret. On top of the password, Pluto opens a notebook from a path or URL, creates a notebook and accepts a notebook connection only from a browser that holds its secret cookie. Pluto sets the cookie on the page you open after signing in. The cookie is
SameSite=Strict, so a link or form on another website cannot carry it. - Unusual paths. A request whose path contains
//gets400from nginx before the password is checked. Pluto reads a path such as//newdifferently in its secret check and in its router, so nginx never lets one through. - Cross-site requests. Any request that changes something (anything but
GET,HEADorOPTIONS) and every notebook connection must come from Pluto's own page. With another site'sOriginheader, nginx answers403before Pluto sees the request. - Request limits. Each client address may make 20 requests a second (bursts of 200, because opening Pluto loads many files at once) and hold 128 connections. Over the limit, nginx answers
429. nginx replaces anyX-Forwarded-Forheader with the address it accepted the connection from and blanksX-Real-IP,Forwardedand similar headers, so a client cannot change the address the limits use. If you put a load balancer in front of the instance, every client shares the load balancer's address for the limits. - Sizes and methods. Notebook uploads are limited to 64 MiB.
TRACEandCONNECTget405. - Headers. Responses carry
Strict-Transport-Security,X-Content-Type-Options: nosniff,X-Frame-Options: SAMEORIGIN, aContent-Security-Policyofframe-ancestors 'self',Referrer-Policy: same-originand aPermissions-Policythat turns off the payment and USB APIs. The camera and microphone stay available to notebook widgets you write. nginx does not advertise its version.
What is logged. nginx's access log (/var/log/nginx/access.log) records the client address, request path and status, without query strings. nginx's error log records only start-up problems, not refused requests, so a password in a rejected request never reaches the disk. Pluto logs to the journal (journalctl -u pluto), and the image's log filter withholds any message that would contain Pluto's secret.
What Pluto fetches from the internet. The editor page loads some optional resources from public servers in your browser: the featured notebook list from featured.plutojl.org and maths fonts from the jsDelivr CDN. It also sends an anonymous usage ping to stats.plutojl.org. The notebooks, your code and your data stay on the instance.
Back up and restore
Everything Pluto keeps is in /var/lib/pluto. That includes your notebooks and the packages and compiled code they installed. Take an EBS snapshot of the root volume, or copy the notebooks off the instance. The packages are re-installed from each notebook's own package list when you open it:
sudo tar -C /var/lib/pluto -czf pluto-notebooks-$(date +%F).tar.gz notebooksTo restore into a new instance of this image, unpack the archive as pluto: sudo -u pluto tar -C /var/lib/pluto -xzf - < pluto-notebooks-<date>.tar.gz. Keep your notebooks in git as well, because they are plain Julia files.
Security notes and the build record
The certificate generated at first boot is a server certificate only. It cannot sign other certificates. Verify this with:
openssl x509 -in /etc/ssl/pluto/pluto.crt -noout -ext basicConstraints,keyUsage,extendedKeyUsage | tee /dev/stderr | grep -q 'CA:FALSE'X509v3 Basic Constraints: critical CA:FALSE X509v3 Key Usage: critical Digital Signature, Key Encipherment X509v3 Extended Key Usage: TLS Web Server AuthenticationJulia is the 1.13.3 release, Pluto is 1.1.0 as pinned by the Manifest, and the notebook account has no login shell and no sudo. Verify all three with:
/opt/julia/julia-1.13.3/bin/julia --version | tee /dev/stderr | grep -qx 'julia version 1.13.3' \
&& awk '/^\[\[deps\.Pluto\]\]$/ {f=1; next} /^\[\[/ {f=0} f && /^version/' /opt/pluto/env/Manifest.toml | tee /dev/stderr | grep -qx 'version = "1.1.0"' \
&& sed -n '1,4p' /usr/share/doc/sac-pluto/BUILD-INFO.txt >&2 \
&& getent passwd pluto | cut -d: -f1,6,7 | tee /dev/stderr | grep -qx 'pluto:/var/lib/pluto:/usr/sbin/nologin' \
&& sudo -l -U pluto 2>&1 | grep -q 'not allowed to run sudo' && echo "Julia 1.13.3, Pluto 1.1.0; pluto: no login shell, no sudo"julia version 1.13.3 version = "1.1.0" pluto Pluto.jl 1.1.0 (github.com/JuliaPluto/Pluto.jl, 2026-10-27), MIT; /opt/pluto/env/Manifest.toml pins it and its 70 dependencies (33 registered packages: MIT, HypertextLiteral ISC; licences in licences/) julia Julia 1.13.3 (official julialang-s3 linux-x86_64 tarball, MIT), /opt/julia/julia-1.13.3 julia-1.13.3-linux-x86_64.tar.gz sha256 5c61cb4b40332cb5a3504e7779e2f5dac093caa4a1116ded19f01413f85c63e4 (= upstream's julia-1.13.3.sha256) pluto:/var/lib/pluto:/usr/sbin/nologin Julia 1.13.3, Pluto 1.1.0; pluto: no login shell, no sudoPluto runs as pluto with no capabilities and NoNewPrivileges. It sees the rest of the system read-only (ProtectSystem=strict, writable only under /var/lib/pluto), has a private /tmp and cannot see other users' processes. It cannot read the credentials file, the password hash or the TLS key, and nginx never passes the password on to it. Pluto's package depot in /opt/julia/depot is owned by root and read-only to notebooks. Packages a notebook installs go to /var/lib/pluto/.julia.
Some sandbox settings are deliberately relaxed because Julia compiles code as it runs. These are JIT compilation (no MemoryDenyWriteExecute), any system call (no SystemCallFilter), a full /proc for its own processes, and netlink sockets.
Do not run Pluto yourself as ubuntu or root (for example julia -e 'using Pluto; Pluto.run()'). That starts a second server without the password, the secret or the metadata block. Manage the service with systemctl and the pluto-* helpers instead.
Julia and Pluto are MIT licensed. Pluto's dependencies are MIT (HypertextLiteral is ISC). Every licence is in /usr/share/doc/sac-pluto/. nginx comes from Ubuntu's main archive.
Keep it up to date
Ubuntu security updates, including nginx, install automatically. To apply all updates now, run:
sudo apt-get update && sudo apt-get upgrade -yPackages your notebooks use update when you change a notebook's package versions in Pluto. Each notebook's package list is stored in the notebook. New Julia and Pluto releases arrive as new versions of the AWS Marketplace listing. Launch the new version, copy /var/lib/pluto/notebooks into it (Step 11), and run sudo pluto-set-site with your domain if you use one. Julia is installed from its release tarball, not from an apt repository, so apt-get upgrade does not change it.
Support
- Email: sales@secureanycloud.com
- Website: https://secureanycloud.com
The other versions on this listing are covered in the developer and API tools guide.
Deploy on AWS
Launch Pluto.jl 1.1.0 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