imgproxy 4.0.22
Overview
This guide covers launching and using the imgproxy 4.0.22 on Ubuntu 24.04 AMI from SecureAnyCloud on Amazon EC2. It is one version of the SecureAnyCloud media servers listing on AWS Marketplace. You pick it as the software version when you subscribe and launch.
imgproxy is a fast, standalone server that resizes, crops and converts images on the fly. Your website or app asks for an image by URL, for example "this photo, 300 pixels wide, as WebP". imgproxy fetches the original from your storage, processes it with the libvips image library and returns the result.
Put a CDN such as Amazon CloudFront in front of it. Every size and format your pages need is then generated once and cached, instead of being prepared ahead of time.
What you get:
- URL signing always on. Every request path must carry an HMAC SHA256 signature made with a key and salt that are generated on your instance's first boot and written to
/root/imgproxy-credentials.txt. Unsigned or wrongly signed requests get HTTP 403, so nobody can use your instance as a free image proxy. No key ships in the image, and imgproxy refuses to start without a real one. - HTTPS from the first boot. nginx serves imgproxy on port 443 with a certificate generated on your instance's first boot. Port 80 redirects to HTTPS and answers
/healthzfor load balancer health checks. - Private and metadata addresses refused as sources. imgproxy will not fetch from loopback, link-local (including the EC2 instance metadata service at 169.254.169.254) or private (RFC1918 and the IPv6 metadata address) addresses, even through a DNS name or a redirect. Upstream imgproxy allows private addresses by default. SecureAnyCloud ships stricter.
- libvips 8.18.7 built from source on Ubuntu main. Ubuntu 24.04's own libvips is an older, community-maintained (universe) package. SecureAnyCloud compiles the current libvips release from its checksum-verified source against Ubuntu main libraries only, so every library on the image receives Ubuntu's standard security updates.
- imgproxy on loopback. imgproxy listens only on
127.0.0.1:8080. nginx is the only service on the network. - Hardened defaults. imgproxy runs as a dynamic unprivileged user in a strict systemd sandbox. imgproxy and nginx are both blocked from the instance metadata service, only GET and HEAD requests reach imgproxy, and the AMI requires IMDSv2.
- Fully patched Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on.
- 24/7 SecureAnyCloud support
The image contains the following:
- imgproxy 4.0.22 in /opt/imgproxy/bin, compiled by SecureAnyCloud from the v4.0.22 release
- libvips 8.18.7 in /opt/imgproxy/libvips, compiled by SecureAnyCloud, with its complete source in /usr/local/src/imgproxy-sac
- nginx, and a 1200 x 800 sample image for the built-in demo
- Ubuntu 24.04 LTS, cloud-init and the AWS Systems Manager agent
Image formats: JPEG, PNG, WebP, AVIF and TIFF in and out. SVG, GIF and HEIC (iPhone photos) in. GIF and HEIC output and JPEG XL are not included, because the libraries they need are not in Ubuntu main.
- 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 with a route to the internet (imgproxy fetches your source images over the network)
- A security group that allows the following inbound traffic:
- TCP 22 (SSH) from your own networks only
- TCP 443 (imgproxy over HTTPS) from your CDN or your users, usually anywhere
- TCP 80 (redirect to HTTPS and
/healthz) from the same sources as 443
Image processing is CPU-bound. An m5.large handles tens of typical web resizes per second. Put a CDN in front so each size is processed only once, and choose a compute-optimised instance (c5, c6i, c7i) for heavier traffic.
Launch the instance
In the AWS Marketplace listing, choose Continue to Subscribe, then Continue to Configuration. Pick imgproxy 4.0.22 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 above.
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 \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=imgproxy}]'Connect to your instance
Connect over SSH as the ubuntu user, which is the SSH user for imgproxy 4.0.22 on Ubuntu 24.04.
ssh -i <your-key.pem> ubuntu@<public-ip>Check the services
First boot generates the certificate, the signing key and the salt, then starts imgproxy and nginx. Check the units:
systemctl is-active imgproxy nginx imgproxy-firstboot
sudo test -f /var/lib/sac/data/imgproxy-firstboot.done && echo FIRSTBOOT_DONE /opt/imgproxy/bin/imgproxy versionAll three units are active, the first boot marker is present and imgproxy reports its version. Confirm this:
active active active FIRSTBOOT_DONE 4.0.22nginx is the only service on public addresses, and imgproxy listens on loopback. Check the listeners:
sudo ss -lntpH | awk '{split($6, a, "\""); print $4, a[2]}' | sort -u0.0.0.0:22 sshd 0.0.0.0:443 nginx 0.0.0.0:80 nginx 127.0.0.1:8080 imgproxy 127.0.0.53%lo:53 systemd-resolve 127.0.0.54:53 systemd-resolve [::]:22 sshd [::]:443 nginx [::]:80 nginxGet your URL, key and salt
sudo cat /root/imgproxy-credentials.txtThe file is readable by root only. Confirm this:
# imgproxy 4.0.22 - generated on first boot by imgproxy-firstboot.service
# The signing key and salt are unique to this instance; none was baked into the image. Keep this file secret.
# Every request path must carry a signature made with them: unsigned or wrongly signed requests get HTTP 403. IMGPROXY_HOST=ec2-203-0-113-10.compute-1.amazonaws.com IMGPROXY_URL=https://ec2-203-0-113-10.compute-1.amazonaws.com/ IMGPROXY_KEY=<64 hex characters> IMGPROXY_SALT=<64 hex characters> IMGPROXY_SIGNATURE_SIZE=32 IMGPROXY_DEMO_URL=https://ec2-203-0-113-10.compute-1.amazonaws.com/<signature>/rs:fit:300:300/plain/local:///sac-sample.png@png ...IMGPROXY_URLis where imgproxy is served. It uses the instance's public DNS name from EC2 instance metadata; the certificate also names the public and private IP addresses. To use your own DNS name, see Step 9.IMGPROXY_KEYandIMGPROXY_SALTare what your application signs URLs with. They are also in/etc/imgproxy/imgproxy.env, which imgproxy reads.IMGPROXY_DEMO_URLis a ready signed URL for the bundled sample image, resized to fit 300 x 300.
When you need to share the file, for example in a support request, print it with the key and salt masked:
sudo sed -E 's/^(IMGPROXY_(KEY|SALT)=).*/\1********/' /root/imgproxy-credentials.txt | grep '^IMGPROXY_'Make your first requests
Open IMGPROXY_DEMO_URL in your browser. The certificate is self-signed, so the browser asks you to confirm it the first time.
From the instance, run the following. It fetches the demo URL and checks that the result is a real 300 x 200 PNG. It then shows that the same path without a valid signature is refused:
sudo bash -c ' F=/root/imgproxy-credentials.txt H=$(sed -n "s/^IMGPROXY_HOST=//p" $F); D=$(sed -n "s/^IMGPROXY_DEMO_URL=//p" $F) R="--resolve $H:443:127.0.0.1" P=/rs:fit:300:300/plain/local:///sac-sample.png@png curl -sk $R -o /tmp/demo.png -w "signed demo URL -> HTTP %{http_code}, %{content_type}\n" "$D" imgproxy-assert-png /tmp/demo.png 300 200 curl -sk $R -o /dev/null -w "no signature -> HTTP %{http_code}\n" "https://$H$P" curl -sk $R -o /dev/null -w "/insecure (unsigned) -> HTTP %{http_code}\n" "https://$H/insecure$P" rm -f /tmp/demo.png 'signed demo URL -> HTTP 200, image/png OK /tmp/demo.png: valid PNG 300x200, 924 bytes no signature -> HTTP 403 /insecure (unsigned) -> HTTP 403imgproxy-sign-url signs any path with this instance's key. The following processes an image from the public internet (imgproxy's own test image) into a 100 x 50 PNG:
H=$(sudo sed -n 's/^IMGPROXY_HOST=//p' /root/imgproxy-credentials.txt) SRC=https://raw.githubusercontent.com/imgproxy/imgproxy/a08006d11003eef87218a7407b1f21a40f6da615/testdata/geometry.png
U=$(sudo imgproxy-sign-url --url "https://$H" "/rs:fit:100:100/plain/$SRC@png")
curl -sk --resolve "$H:443:127.0.0.1" -o /tmp/geometry.png -w 'HTTP %{http_code}\n' "$U" imgproxy-assert-png /tmp/geometry.png 100 50 rm -f /tmp/geometry.pngHTTP 200 OK /tmp/geometry.png: valid PNG 100x50, 7051 bytesRequest sizes and formats
An imgproxy URL is https://<host>/<signature>/<processing options>/plain/<source URL>@<format>. Common options are rs:<type>:<width>:<height> (resize: fit keeps the whole image, fill crops to the exact size), q:<quality> and g:<gravity> for the crop position. The full list is in the imgproxy documentation.
Request the sample image in several formats and sizes:
H=$(sudo sed -n 's/^IMGPROXY_HOST=//p' /root/imgproxy-credentials.txt) for o in rs:fill:400:400/q:85@webp rs:fit:640:0@avif rs:fill:160:90/g:ce@jpg rs:fit:1200:1200@tiff; do
U=$(sudo imgproxy-sign-url --url "https://$H" "/${o%@*}/plain/local:///sac-sample.png@${o#*@}")
curl -sk --resolve "$H:443:127.0.0.1" -o /dev/null -w "${o} -> %{http_code} %{content_type} %{size_download} bytes\n" "$U" doners:fill:400:400/q:85@webp -> 200 image/webp 2416 bytes rs:fit:640:0@avif -> 200 image/avif 1391 bytes rs:fill:160:90/g:ce@jpg -> 200 image/jpeg 1196 bytes rs:fit:1200:1200@tiff -> 200 image/tiff 2880278 bytesTo serve your own images, use their URL as the source, for example /rs:fit:800:0/plain/https://images.example.com/photos/cat.jpg@webp. If the source URL contains a query string or an @, percent-encode it or use the base64 form /rs:fit:800:0/<base64url of the source URL>.webp.
Sign URLs in your application
Your application builds the URL path, signs it with the key and salt from Step 4 and puts the signature in front. The calculation is:
signature = base64url( HMAC_SHA256( key = hex_decode(IMGPROXY_KEY), message = hex_decode(IMGPROXY_SALT) + path ) ) with the trailing "=" padding removed URL = https://<host>/<signature><path>Here is a Python example. Keep the key and salt in your secrets store, never in client-side code:
import base64, hashlib, hmac def imgproxy_url(host, key_hex, salt_hex, path): digest = hmac.new(bytes.fromhex(key_hex), bytes.fromhex(salt_hex) + path.encode(), hashlib.sha256).digest() signature = base64.urlsafe_b64encode(digest).rstrip(b"=").decode() return f"https://{host}/{signature}{path}" print(imgproxy_url("img.example.com", KEY, SALT, "/rs:fit:300:300/plain/https://images.example.com/cat.jpg@webp"))The following runs the same calculation on the instance and confirms that it matches imgproxy-sign-url:
sudo python3 - <<'PY' import base64, hashlib, hmac, subprocess env = dict(l.split("=", 1) for l in open("/root/imgproxy-credentials.txt").read().splitlines() if l and not l.startswith("#")) path = "/rs:fit:300:300/plain/local:///sac-sample.png@png" digest = hmac.new(bytes.fromhex(env["IMGPROXY_KEY"]), bytes.fromhex(env["IMGPROXY_SALT"]) + path.encode(), hashlib.sha256).digest() mine = "/" + base64.urlsafe_b64encode(digest).rstrip(b"=").decode() + path helper = subprocess.run(["imgproxy-sign-url", path], capture_output=True, text=True).stdout.strip() print("signatures match" if mine == helper else "MISMATCH") PYsignatures matchClient libraries that implement this exist for most languages. See the imgproxy documentation on signing.
Allow only your own image sources
By default, a correctly signed URL may point at any public address. Only holders of your key can create one, but pinning the sources is good practice.
Edit /etc/imgproxy/imgproxy.env and list your origins, comma-separated, each as a URL prefix. Then restart imgproxy:
sudo nano /etc/imgproxy/imgproxy.env
# IMGPROXY_ALLOWED_SOURCES=https://images.example.com/,https://my-bucket.s3.amazonaws.com/,local://
sudo systemctl restart imgproxyTo serve images straight from a private S3 bucket, imgproxy also supports s3:// sources with IMGPROXY_USE_S3=true. It then needs AWS credentials.
Because imgproxy is blocked from the instance metadata service on this image, use an access key in /etc/imgproxy/imgproxy.env (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION) for a dedicated IAM user that can only read that bucket. Alternatively, make the bucket reachable through CloudFront and use https:// sources.
Use your own DNS name and certificate
Point a DNS record at the instance (an Elastic IP keeps the address stable), then tell the instance its name. This regenerates the self-signed certificate for that name, updates the credentials file and reloads nginx:
sudo imgproxy-set-hostname img.your-domain.exampleTo use a certificate from a public CA instead, replace /etc/ssl/imgproxy/imgproxy.crt (full chain) and /etc/ssl/imgproxy/imgproxy.key (mode 0600) and run sudo systemctl reload nginx.
Behind CloudFront or an Application Load Balancer, point the origin at port 443 and the health check at /healthz.
Rotate the signing key
To replace the key and salt (for example after a staff change), run the rotation helper. It writes a new pair to /etc/imgproxy/imgproxy.env and the credentials file, and it restarts imgproxy.
Every URL signed with the old key stops working, so update your application's secret at the same time:
sudo imgproxy-rotate-keys
H=$(sudo sed -n 's/^IMGPROXY_HOST=//p' /root/imgproxy-credentials.txt) sleep 2 curl -sk --resolve "$H:443:127.0.0.1" -o /dev/null -w 'new demo URL -> HTTP %{http_code}\n' "$(sudo sed -n 's/^IMGPROXY_DEMO_URL=//p' /root/imgproxy-credentials.txt)"imgproxy-rotate-keys: new signing key and salt in place; URLs signed with the previous key are now refused new demo URL -> HTTP 200Security checks
A correctly signed request for the instance metadata service, a loopback address or a private address is refused, while a public source works (Step 5). Verify this:
H=$(sudo sed -n 's/^IMGPROXY_HOST=//p' /root/imgproxy-credentials.txt)
T=$(date '+%Y-%m-%d %H:%M:%S') for src in http://169.254.169.254/latest/meta-data/ http://127.0.0.1:8080/health http://10.0.0.1/photo.jpg; do
U=$(sudo imgproxy-sign-url --url "https://$H" "/rs:fit:50:50/plain/$src@png")
curl -sk --resolve "$H:443:127.0.0.1" -o /dev/null -w "$src -> HTTP %{http_code}\n" "$U" done
sudo journalctl -u imgproxy -o cat --since "$T" | grep -oE '(Link-local|Loopback|Private) source address is not allowed' | sort | uniq -chttp://169.254.169.254/latest/meta-data/ -> HTTP 404 http://127.0.0.1:8080/health -> HTTP 404 http://10.0.0.1/photo.jpg -> HTTP 404 1 Link-local source address is not allowed 1 Loopback source address is not allowed 1 Private source address is not allowedimgproxy and nginx are also blocked from the instance metadata service at the network level. imgproxy runs sandboxed as a dynamic user, with a guard that refuses to start it without a real signing key. Verify this:
systemctl show imgproxy nginx -p IPAddressDeny --value
systemctl show imgproxy -p DynamicUser -p ProtectSystem -p ExecStartPre --value | cut -c1-80 grep -E '^IMGPROXY_(BIND|ALLOW_[A-Z_]+)=' /etc/imgproxy/imgproxy.env.tmplfd00:ec2::254/128 168.63.129.16/32 169.254.169.254/32 fd00:ec2::254/128 168.63.129.16/32 169.254.169.254/32 { path=/usr/local/sbin/imgproxy-config-guard ; argv[]=/usr/local/sbin/imgproxy-c yes strict IMGPROXY_BIND=127.0.0.1:8080 IMGPROXY_ALLOW_LINK_LOCAL_SOURCE_ADDRESSES=false IMGPROXY_ALLOW_LOOPBACK_SOURCE_ADDRESSES=false IMGPROXY_ALLOW_PRIVATE_SOURCE_ADDRESSES=falsesystemd may list the three denied addresses in a different order. What matters is that all three appear for both units.
Maintenance
- Settings:
/etc/imgproxy/imgproxy.env(every option is anIMGPROXY_environment variable, see the configuration reference). Apply changes withsudo systemctl restart imgproxy. KeepIMGPROXY_BIND=127.0.0.1:8080, because nginx publishes it. - Logs: imgproxy logs every request to the journal (
journalctl -u imgproxy) with the real client address. nginx logs are in/var/log/nginx/. - Health checks:
GET /healthzon port 80 or 443 returnsokwithout a signature. - Limits: source images are limited to 50 megapixels and 40 MB, downloads to 10 seconds and processing to 20 seconds. Raise
IMGPROXY_MAX_SRC_RESOLUTION,IMGPROXY_MAX_SRC_FILE_SIZEand the timeouts in the settings file if you need larger originals. - Security updates: unattended security upgrades are on and cover nginx and every library libvips uses, all from Ubuntu main. Reboot when a new kernel is installed. imgproxy and libvips themselves are updated by a new version of this image.
Licences and source code
imgproxy is open-source software licensed under the Apache License 2.0. Its licence and notice are in /usr/share/doc/imgproxy-sac/. The licences of every Go module compiled into it are in /usr/share/doc/imgproxy-sac/third-party/.
libvips is licensed under the GNU Lesser General Public License version 2.1 or later. It is dynamically linked from /opt/imgproxy/libvips. Its complete source, the release archive this build was compiled from, is in /usr/local/src/imgproxy-sac/, so you can rebuild and replace it. Build details are in /usr/share/doc/imgproxy-sac/README.sac.
Support
SecureAnyCloud provides 24/7 technical support for this image. Contact sales@secureanycloud.com.
This image is provided by SecureAnyCloud and is not affiliated with or endorsed by imgproxy or the libvips project.
Deploy on AWS
Launch imgproxy 4.0.22 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