BaGetter 1.6.5
Overview
BaGetter is a lightweight, self-hosted NuGet server. Your build pipelines push .nupkg packages to it, and dotnet restore, Visual Studio and the nuget client install them from it with the standard NuGet v3 protocol. Internal .NET libraries are then shared the same way that public ones are from nuget.org.
This SecureAnyCloud AMI runs BaGetter 1.6.5 on Ubuntu 24.04 on Amazon EC2. It is one version of the SecureAnyCloud artifact and container registries listing on AWS Marketplace. You choose it as the software version when you subscribe and launch.
Upstream BaGetter ships with no push API key. An empty key means that anyone who can reach the server may push and delete packages, and TLS and serving are left to you. The SecureAnyCloud image turns BaGetter into a ready-to-use private feed.
What you get:
- TLS from the first boot. nginx serves the feed on port 443 (
https://<host>/v3/index.json) with a certificate generated on your instance's first boot, naming the instance's DNS name and IP addresses. The certificate is a server (leaf) certificate, not a certificate authority, so trusting it on a client trusts this one server only. BaGetter itself listens on127.0.0.1only, and nothing listens on port 80. - A push API key unique to your instance. Pushing, deleting and relisting packages need the
X-NuGet-ApiKeykey, which is generated on the first boot and written to/root/bagetter-credentials.txt(root only). BaGetter and nginx refuse to start without a real key, so the feed can never come up open for pushes. Reading the feed (restore, search, the web gallery) is open to whoever can reach port 443, as a NuGet feed must be. - Your packages on their own disk. The SQLite database and the package store live on a separate 10 GiB gp3 data volume mounted at
/var/lib/bagetter, which you can resize on its own. - Safe defaults. Mirroring of nuget.org is off, so the instance makes no outbound package fetches. Pushing an existing version again is refused instead of overwriting it, and deleting a version unlists it.
- Fail-closed lockdown. Requests for host names that the instance does not serve are dropped. BaGetter runs as an unprivileged user in a sandboxed service. BaGetter, nginx and the first-boot service are blocked from the instance metadata service, and the AMI requires IMDSv2.
- Built from source on a supported runtime. BaGetter 1.6.5 is compiled from the upstream release (pinned by SHA-256) for the supported .NET 10 runtime from Ubuntu. Its dependencies are raised to versions that fix every published advisory in the upstream set. The base is fully patched Ubuntu 24.04 LTS with unattended security upgrades switched on.
- The full stack on the image. BaGetter 1.6.5 (
/opt/bagetter, with licence and build provenance in/usr/share/doc/bagetter), the ASP.NET Core 10 runtime, nginx, Ubuntu 24.04 LTS, cloud-init and the AWS Systems Manager agent. No .NET SDK is installed.
- 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 that your build machines can reach.
- A security group that allows TCP 22 (SSH) from your own networks only, and TCP 443 from the machines that push and restore packages. Nothing else needs to be open.
Launch the instance
In the AWS Marketplace listing, choose Continue to Subscribe, then Continue to Configuration. Pick BaGetter 1.6.5 on Ubuntu 24.04 as the software version, select your Region, and choose Launch through EC2.
Choose m5.large, your key pair and a security group as described in Prerequisites. The AMI attaches the 10 GiB data volume for /var/lib/bagetter automatically. If you plan to keep many packages, increase its size in the storage section of the launch wizard.
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=bagetter}]'Connect to your instance
Version SSH user
BaGetter 1.6.5 on Ubuntu 24.04 ubuntu
ssh -i <your-key.pem> ubuntu@<public-ip>Check the services
On first boot, the instance generates the certificate and the push API key. Then BaGetter and nginx start. Confirm that they are running:
systemctl is-active bagetter nginx
sudo test -f /var/lib/sac/data/bagetter-firstboot.done && echo FIRSTBOOT_DONEactive active FIRSTBOOT_DONEOnly SSH and nginx listen on the network. BaGetter is on loopback behind nginx, and the DNS resolver listens on loopback only. Check the listeners:
sudo ss -lntp | awk 'NR>1 {print $4, $6}' | sed -E 's/pid=[0-9]+,fd=[0-9]+/.../g' | sort0.0.0.0:22 users:(("sshd",...),("systemd",...)) 0.0.0.0:443 users:(("nginx",...),("nginx",...),("nginx",...)) 127.0.0.1:5000 users:(("dotnet",...)) 127.0.0.53%lo:53 users:(("systemd-resolve",...)) 127.0.0.54:53 users:(("systemd-resolve",...)) [::]:22 users:(("sshd",...),("systemd",...)) [::]:443 users:(("nginx",...),("nginx",...),("nginx",...))The package store is on its own volume. Confirm the mount:
df -h /var/lib/bagetter | awk 'NR==2 {print $2, $6}'9.8G /var/lib/bagetterGet your feed URL and API key
The credentials file is readable by root only. The following command prints the file with the key hidden. To see the key itself, run sudo cat /root/bagetter-credentials.txt:
sudo sed -E 's/^(BAGETTER_API_KEY=).*/\1<hidden>/' /root/bagetter-credentials.txt# BaGetter 1.6.5 - generated on first boot by bagetter-firstboot.service
# This push API key is unique to this instance; none was baked into the image. Keep this file secret.
# Reading the feed (restore, browse) needs no key; pushing, deleting and relisting packages need BAGETTER_API_KEY. BAGETTER_URL=https://ec2-44-201-173-7.compute-1.amazonaws.com/ BAGETTER_SERVICE_INDEX=https://ec2-44-201-173-7.compute-1.amazonaws.com/v3/index.json BAGETTER_API_KEY=<hidden> BAGETTER_CA_CERT=/etc/nginx/tls/bagetter.crt BAGETTER_CERT_SHA256=D6:35:B4:B5:78:D4:14:36:1C:A8:34:A9:C0:32:E7:3A:7D:29:45:E4:9B:01:44:87:CD:85:E8:6A:B0:C1:85:35
#
# Push: dotnet nuget push <package>.nupkg --source <BAGETTER_SERVICE_INDEX> --api-key <BAGETTER_API_KEY>
# Restore: dotnet nuget add source <BAGETTER_SERVICE_INDEX> --name bagetter
# The certificate is self-signed: trust /etc/nginx/tls/bagetter.crt on your clients, or use your own (see the guide).
# New push API key:
sudo bagetter-rotate-apikey
# Change the name it serves as:
sudo bagetter-set-hostname <your-dns-name>
# User guide: https://secureanycloud.com/stacks/bagetter-1-6-5/The output shows your instance's own DNS name and certificate hash. The service index advertises every NuGet endpoint under that name. To serve under your own name, see Step 8:
curl -s --cacert /etc/nginx/tls/bagetter.crt https://localhost/v3/index.json | jq -r '.resources[] | "\(.["@type"]) \(.["@id"])"' | sort -u | head -6PackageBaseAddress/3.0.0 https://ec2-44-201-173-7.compute-1.amazonaws.com/v3/package/ PackagePublish/2.0.0 https://ec2-44-201-173-7.compute-1.amazonaws.com/api/v2/package RegistrationsBaseUrl https://ec2-44-201-173-7.compute-1.amazonaws.com/v3/registration/ RegistrationsBaseUrl/3.0.0-beta https://ec2-44-201-173-7.compute-1.amazonaws.com/v3/registration/ RegistrationsBaseUrl/3.0.0-rc https://ec2-44-201-173-7.compute-1.amazonaws.com/v3/registration/ SearchAutocompleteService https://ec2-44-201-173-7.compute-1.amazonaws.com/v3/autocompletePush and find a package
The image carries a small smoke-test package, /opt/bagetter/testdata/Sac.Smoke.Test.1.0.0.nupkg. The following commands push it on the instance itself, first without the key and then with it. The key is handed to curl on standard input, so it never appears in the process list:
sudo bash -c ' K=$(grep "^BAGETTER_API_KEY=" /root/bagetter-credentials.txt | cut -d= -f2-) P=/opt/bagetter/testdata/Sac.Smoke.Test.1.0.0.nupkg curl -s --cacert /etc/nginx/tls/bagetter.crt -o /dev/null -w "push without the key -> HTTP %{http_code}\n" -X PUT -F package=@$P https://localhost/api/v2/package printf "header = \"X-NuGet-ApiKey: %s\"\n" "$K" | curl -K - -s --cacert /etc/nginx/tls/bagetter.crt -o /dev/null -w "push with the key -> HTTP %{http_code}\n" -X PUT -F package=@$P https://localhost/api/v2/package'push without the key -> HTTP 401 push with the key -> HTTP 201A second push of the same version answers 409, because versions are never overwritten. The package is now in the feed:
curl -s --cacert /etc/nginx/tls/bagetter.crt "https://localhost/v3/search?q=sac" | jq -c '.data[] | {id, version}'
curl -s --cacert /etc/nginx/tls/bagetter.crt https://localhost/v3/package/sac.smoke.test/index.json; echo{"id":"Sac.Smoke.Test","version":"1.0.0"} {"versions":["1.0.0"]}The web gallery at https://<host>/ lists the packages in the feed, with search. It is read-only and needs no login:
Each package page gives the install command for the .NET CLI, PackageReference, Paket and the Package Manager console, along with its versions and a download link:
The Upload page shows the push command for your feed's service index. Add --api-key with the key from Step 4 (see Step 6):
To remove the test package from search and restore again, unlist it with the key. DELETE unlists a package but keeps the file, so builds that pinned it keep working:
sudo bash -c ' K=$(grep "^BAGETTER_API_KEY=" /root/bagetter-credentials.txt | cut -d= -f2-) printf "header = \"X-NuGet-ApiKey: %s\"\n" "$K" | curl -K - -s --cacert /etc/nginx/tls/bagetter.crt -o /dev/null -w "unlist -> HTTP %{http_code}\n" -X DELETE https://localhost/api/v2/package/Sac.Smoke.Test/1.0.0'unlist -> HTTP 204Use the feed from your build machines
Copy the instance's certificate (/etc/nginx/tls/bagetter.crt) to your build machine and compare its fingerprint with BAGETTER_CERT_SHA256 from Step 4. It is a leaf server certificate for this instance's names only (CA:FALSE, no certificate signing). Adding it to a machine's trust store lets that machine trust this feed and nothing else.
On an Ubuntu or Debian build machine, run:
scp -i <your-key.pem> ubuntu@<public-ip>:/etc/nginx/tls/bagetter.crt ./bagetter.crt openssl x509 -noout -fingerprint -sha256 -in ./bagetter.crt
sudo install -m 0644 ./bagetter.crt /usr/local/share/ca-certificates/bagetter.crt
sudo update-ca-certificatesThen add the feed and push from your workstation or CI job. Use your instance's DNS name (or your own, Step 8) in place of <your-domain>, and use the key from Step 4. In CI, keep the key in the pipeline's secret store rather than in a file in the repository:
dotnet nuget add source https://<your-domain>/v3/index.json --name bagetter dotnet nuget push ./bin/Release/MyCompany.Utils.1.2.0.nupkg --source bagetter --api-key "$BAGETTER_API_KEY" dotnet add package MyCompany.Utils --source bagetterIf you use a certificate from your own certificate authority (Step 8), you do not need the trust step.
Check the security posture
A wrong key is refused for pushes and deletes. The feed and the health check stay readable. A request for a host name that the instance does not serve is dropped without an answer, and nothing answers on port 80:
C=/etc/nginx/tls/bagetter.crt curl -s --cacert $C -o /dev/null -w 'service index -> HTTP %{http_code}\n' https://localhost/v3/index.json curl -s --cacert $C -K <(printf 'header = "X-NuGet-ApiKey: not-the-key"\n') -X PUT -F package=@/opt/bagetter/testdata/Sac.Smoke.Test.1.0.0.nupkg -o /dev/null -w 'push, wrong key -> HTTP %{http_code}\n' https://localhost/api/v2/package curl -s --cacert $C -X DELETE -o /dev/null -w 'delete, no key -> HTTP %{http_code}\n' https://localhost/api/v2/package/Sac.Smoke.Test/1.0.0 curl -s --cacert $C -o /dev/null -w 'health check -> HTTP %{http_code}\n' https://localhost/healthz curl -s --cacert $C -H 'Host: evil.example' -o /dev/null -w 'unknown host -> %{http_code}\n' https://localhost/healthz || true curl -s -o /dev/null -w 'plain http :80 -> %{http_code}\n' --connect-timeout 3 http://127.0.0.1/ || trueservice index -> HTTP 200 push, wrong key -> HTTP 401 delete, no key -> HTTP 401 health check -> HTTP 200 unknown host -> 000 plain http :80 -> 000A status of 000 means no answer. nginx closes the connection for a host name that it does not serve, and nothing listens on port 80.
The certificate is a leaf certificate for the instance's names:
openssl x509 -noout -ext subjectAltName,basicConstraints,keyUsage,extendedKeyUsage -in /etc/nginx/tls/bagetter.crtX509v3 Subject Alternative Name: DNS:ec2-44-201-173-7.compute-1.amazonaws.com, IP Address:44.201.173.7, IP Address:172.31.80.97, IP Address:127.0.0.1, DNS:localhost X509v3 Basic Constraints: critical CA:FALSE X509v3 Key Usage: critical Digital Signature, Key Encipherment X509v3 Extended Key Usage: TLS Web Server AuthenticationBaGetter runs as the unprivileged bagetter user on loopback. BaGetter, nginx and the first-boot service cannot reach the instance metadata service:
ps -o user:8=,args= -C dotnet systemctl show bagetter nginx bagetter-firstboot -p Id -p IPAddressDenybagetter /usr/bin/dotnet /opt/bagetter/BaGetter.dll IPAddressDeny=fd00:ec2::254/128 168.63.129.16/32 169.254.169.254/32 Id=bagetter.service IPAddressDeny=fd00:ec2::254/128 168.63.129.16/32 169.254.169.254/32 Id=nginx.service IPAddressDeny=fd00:ec2::254/128 168.63.129.16/32 169.254.169.254/32 Id=bagetter-firstboot.serviceRotate the key, use your own DNS name
Run sudo bagetter-rotate-apikey to replace the push API key with a new random one. The command writes the new key to /root/bagetter-credentials.txt and restarts BaGetter, and the old key stops working at once. Update the key in your CI secret store afterwards. Rotate the key whenever it may have been exposed.
To serve the feed under your own name, point a DNS record at the instance and then set the name. The command generates a new self-signed leaf certificate for the name (and the instance's IP addresses), updates nginx and the URLs in the credentials file, and keeps the name across reboots. Give your build machines the new certificate afterwards:
sudo bagetter-set-hostname <your-domain>To use a certificate from your own certificate authority instead, copy it over the generated pair and reload nginx. Running bagetter-set-hostname again replaces it with a self-signed certificate:
sudo install -m 0644 <your-domain>.crt /etc/nginx/tls/bagetter.crt
sudo install -m 0600 <your-domain>.key /etc/nginx/tls/bagetter.key
sudo nginx -t && sudo systemctl reload nginxServer components
Component Path
BaGetter application /opt/bagetter/ (read only, owner root)
Licence and build provenance /usr/share/doc/bagetter/
Database and package store /var/lib/bagetter/ (data volume: bagetter.db, packages/)
Push API key /etc/bagetter/bagetter.env (mode 0600, read by systemd)
Service unit /etc/systemd/system/bagetter.service (loopback only; settings as environment variables)
nginx site /etc/nginx/sites-available/sac-bagetter
Certificate and key /etc/nginx/tls/
First boot script /usr/local/sbin/bagetter-firstboot.sh
Helpers /usr/local/sbin/bagetter-rotate-apikey, bagetter-set-hostname, bagetter-guard
Credentials /root/bagetter-credentials.txt (mode 0600)
dotnet --list-runtimes grep -E '^(source|SDK|NuGet audit)' /usr/share/doc/bagetter/BUILD-PROVENANCE.txt nginx -v 2>&1Microsoft.AspNetCore.App 10.0.14 [/usr/lib/dotnet/shared/Microsoft.AspNetCore.App] Microsoft.NETCore.App 10.0.14 [/usr/lib/dotnet/shared/Microsoft.NETCore.App] source: https://github.com/bagetter/BaGetter/archive/refs/tags/v1.6.5.tar.gz SDK: .NET SDK 10.0.114 (Ubuntu 24.04 dotnet-sdk-10.0, purged after the build) NuGet audit: no vulnerable packages (dotnet list package --vulnerable --include-transitive) nginx version: nginx/1.24.0 (Ubuntu)BaGetter's settings (storage path, database, mirroring off, overwrites refused) are environment variables in bagetter.service. To change one, run sudo systemctl edit bagetter and use the names from the BaGetter configuration documentation. The .NET runtime is an Ubuntu package and is patched with the rest of the system.
For logs, run sudo journalctl -u bagetter for BaGetter, and read /var/log/nginx/access.log for requests (with the real client address).
Security recommendations
- Allow port 22 in the security group from your own networks only, and port 443 only from the networks of your build machines and developers.
- Keep the push API key in your CI system's secret store. Rotate it (Step 8) when someone leaves or when it may have been exposed, because anyone with the key can publish packages that your builds will install.
- Back up
/var/lib/bagetter. An EBS snapshot of the data volume is the simplest method. - Use your own DNS name and certificate (Step 8).
- Keep the operating system patched. Unattended security upgrades are on, and
sudo apt-get update && sudo apt-get upgradeapplies everything else.
Support and licensing
BaGetter is open source under the MIT License, and there is no per-user or per-CPU licence fee. The licence is in /usr/share/doc/bagetter/. SecureAnyCloud is not affiliated with or endorsed by the BaGetter project. The AMI price covers SecureAnyCloud's hardening, first-boot setup and support.
- Email: sales@secureanycloud.com
- Website: https://secureanycloud.com
- Support: 24/7, with a 24-hour response SLA
Deploy on AWS
Launch BaGetter 1.6.5 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