NuGet Repositories, API Keys, Feeds, Symbols, Proxying, and .NET Package Workflows: Guided Hands-On Workflow and Core Operations
Build a disposable NuGet hosted/proxy/group workflow, isolate NuGet configuration and package caches, proxy a harmless public dependency, publish a synthetic nupkg and snupkg, restore through a group, inspect metadata, and rotate the lab API key safely.
Learning objectives
- Create disposable NuGet hosted/proxy/group repositories and explain every state change.
-
Use an isolated
NuGet.ConfigandNUGET_PACKAGESdirectory. - Proxy a harmless public package, build a synthetic package, push to hosted, and restore through a group.
- Use environment-backed restore credentials and a temporary NuGet API key without writing secrets into project files.
-
Create and validate a companion
.snupkgon Nexus 3.95+.
Lab scope. Disposable Nexus Repository Community
Edition 3.95.2 on 127.0.0.1:8081, Java 21 on the
server, .NET SDK 10.0.400 on the client. Commands are POSIX/Bash;
PowerShell uses $env:NAME for environment variables and
Get-FileHash -Algorithm SHA256. HTTP is explicitly
permitted only for loopback. Production uses HTTPS.
1. Preflight and complete client isolation
export NX_URL="http://127.0.0.1:8081"
export LAB="${TMPDIR:-/tmp}/nexus-ch10"
rm -rf "$LAB"
mkdir -p "$LAB/evidence" "$LAB/packages" "$LAB/public-cache" "$LAB/internal-cache" "$LAB/src"
chmod 700 "$LAB"
export NUGET_PACKAGES="$LAB/packages"
export NUGET_HTTP_CACHE_PATH="$LAB/http-cache"
export NUGET_SCRATCH="$LAB/scratch"
dotnet --info | tee "$LAB/evidence/dotnet-info.txt"
curl -fsS "$NX_URL/service/rest/v1/status" | tee "$LAB/evidence/nexus-status.txt"
curl -fsS "$NX_URL/service/rest/v1/repositories" | tee "$LAB/evidence/repositories-before.json"
The global package folder, HTTP cache, scratch state, and config are all lab-scoped. This prevents a pre-existing global package from hiding whether Nexus actually served a request.
2. Create the disposable NuGet topology
In Nexus UI create:
| Repository | Type | Settings |
|---|---|---|
academy-ch10-hosted |
NuGet hosted |
Blob default;
Disable redeploy; online.
|
academy-ch10-proxy |
NuGet proxy |
Protocol version 3; remote
https://api.nuget.org/v3/index.json; default
cache settings.
|
academy-ch10-group |
NuGet group | Members hosted first, proxy second. |
Activate the NuGet API-Key Realm if it is not
active. Create a disposable publisher identity with read/browse on
the group and add/read/browse privileges on the hosted repository,
plus the current nx-apikey-all privilege required to
access a NuGet API key. Do not use the administrator account as the
package publisher.
3. Write a one-source V3 NuGet.Config
export NUGET_CONFIG="$LAB/NuGet.Config"
cat > "$NUGET_CONFIG" <<'EOF'
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="AcademyGroup"
value="http://127.0.0.1:8081/repository/academy-ch10-group/index.json"
protocolVersion="3"
allowInsecureConnections="true" />
</packageSources>
</configuration>
EOF
chmod 600 "$NUGET_CONFIG"
dotnet nuget list source --configfile "$NUGET_CONFIG" | tee "$LAB/evidence/sources.txt"
The config contains routing but no credential. NuGet will ask its
credential chain for AcademyGroup when Nexus responds
with 401.
4. Supply read credentials from an environment variable
read -r -p 'Disposable Nexus username: ' NX_USER
read -r -s -p 'Disposable Nexus password: ' NX_PASS; echo
export NuGetPackageSourceCredentials_AcademyGroup="Username=${NX_USER};Password=${NX_PASS};ValidAuthenticationTypes=Basic"
unset NX_PASS
The source name in the variable must exactly match
AcademyGroup. NuGet gives this environment variable
higher credential precedence than config-file credentials. Do not
print the variable into evidence.
5. Prove a public package crosses the proxy boundary
Create a tiny disposable console project that references
Newtonsoft.Json 13.0.3. The first restore should cause
Nexus to fetch metadata/package bytes through
academy-ch10-proxy; later restores can be served from
Nexus cache, while the local NuGet cache remains a separate layer.
mkdir -p "$LAB/src/public-consumer"
cd "$LAB/src/public-consumer"
dotnet new console --framework net10.0 --no-restore
cat > public-consumer.csproj <<'EOF'
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<NuGetAudit>false</NuGetAudit>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
</Project>
EOF
rm -rf "$LAB/public-cache" && mkdir -p "$LAB/public-cache"
NUGET_PACKAGES="$LAB/public-cache" dotnet restore --configfile "$NUGET_CONFIG" --no-http-cache --force-evaluate -v normal 2>&1 | tee "$LAB/evidence/public-restore.txt"
Browse/search the proxy in Nexus after the restore. That evidence
proves proxy cache population; the local
public-cache proves client-side consumption.
6. Build a synthetic immutable package and symbols
mkdir -p "$LAB/src/widget"
cd "$LAB/src/widget"
dotnet new classlib --framework net10.0 --name Learner.Ch10.Widget --no-restore
cd Learner.Ch10.Widget
cat > Learner.Ch10.Widget.csproj <<'EOF'
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<PackageId>Learner.Ch10.Widget</PackageId>
<Version>1.0.0</Version>
<Authors>DevOps Academy Lab</Authors>
<Description>Disposable Nexus NuGet training package</Description>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
<Deterministic>true</Deterministic>
<NuGetAudit>false</NuGetAudit>
</PropertyGroup>
</Project>
EOF
cat > Class1.cs <<'EOF'
namespace Learner.Ch10.Widget;
public static class Identity
{
public static string Value => "Learner.Ch10.Widget/1.0.0";
}
EOF
dotnet restore --configfile "$NUGET_CONFIG" -p:NuGetAudit=false
dotnet pack -c Release --no-restore -o "$LAB/pkg" 2>&1 | tee "$LAB/evidence/pack.txt"
ls -lh "$LAB/pkg" | tee "$LAB/evidence/package-files.txt"
The package and symbol package come from one build. The version is not rebuilt later for “promotion.”
7. Record package hashes before publication
python - <<'PYI' | tee "$LAB/evidence/producer-sha256.txt"
from pathlib import Path
import hashlib, os
for p in sorted(Path(os.environ["LAB"], "pkg").glob("*.*nupkg")):
print(hashlib.sha256(p.read_bytes()).hexdigest(), p.name)
PYI
8. Obtain a disposable NuGet API key without exposing it
Under the publisher user's profile, open
My Account → NuGet API Key and access/generate the
key. If the view is absent, confirm the realm and
nx-apikey-all privilege first rather than broadening
repository permissions.
read -r -s -p 'Disposable NuGet API key: ' NUGET_API_KEY; echo
export NUGET_API_KEY
Current NuGet 7.6+ clients can read NUGET_API_KEY for
push operations. This avoids placing the key value in the process
argument list.
9. Publish package and symbols only to hosted
export HOSTED_V3="http://127.0.0.1:8081/repository/academy-ch10-hosted/index.json"
dotnet nuget push "$LAB/pkg/Learner.Ch10.Widget.1.0.0.nupkg" --source "$HOSTED_V3" --configfile "$NUGET_CONFIG" 2>&1 | tee "$LAB/evidence/push-package.txt"
dotnet nuget push "$LAB/pkg/Learner.Ch10.Widget.1.0.0.snupkg" --source "$HOSTED_V3" --configfile "$NUGET_CONFIG" 2>&1 | tee "$LAB/evidence/push-symbols.txt"
The API key authenticates deployment, while hosted repository privileges authorize the write. Inspect Nexus Browse/Search to confirm both assets are associated with the hosted repository. Never publish to the proxy or group URL.
10. Consume the internal package through the group from a fresh cache
mkdir -p "$LAB/src/internal-consumer"
cd "$LAB/src/internal-consumer"
cat > internal-consumer.csproj <<'EOF'
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<NuGetAudit>false</NuGetAudit>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Learner.Ch10.Widget" Version="1.0.0" />
</ItemGroup>
</Project>
EOF
rm -rf "$LAB/internal-cache" && mkdir -p "$LAB/internal-cache"
NUGET_PACKAGES="$LAB/internal-cache" dotnet restore --configfile "$NUGET_CONFIG" --no-http-cache --force-evaluate -v normal 2>&1 | tee "$LAB/evidence/internal-restore.txt"
find "$LAB/internal-cache/learner.ch10.widget/1.0.0" -maxdepth 2 -type f -print | tee "$LAB/evidence/internal-cache-files.txt"
Restore through the group proves the consumer endpoint works
independently from the publish endpoint. A fresh
NUGET_PACKAGES path prevents a previous local restore
from masking the request.
11. Deliberately test immutable release rejection
Because the hosted repository uses Disable redeploy, a second push of the exact same ID/version should fail. Preserve the failure; do not “fix” it by enabling redeploy.
set +e
dotnet nuget push "$LAB/pkg/Learner.Ch10.Widget.1.0.0.nupkg" --source "$HOSTED_V3" --configfile "$NUGET_CONFIG" >"$LAB/evidence/duplicate-push.txt" 2>&1
rc=$?
set -e
printf 'duplicate push exit=%s
' "$rc" | tee -a "$LAB/evidence/duplicate-push.txt"
test "$rc" -ne 0
Current Sonatype documentation notes Nexus returns HTTP 400 for an
existing package, so --skip-duplicate is not a reliable
Nexus duplicate-handling strategy. Treat the rejection as evidence
of the release policy.
12. Verify current symbol support
On Nexus 3.95+, the uploaded .snupkg should appear in
hosted repository state and symbol server functionality is
available. Record the group symbol endpoint as:
http://127.0.0.1:8081/repository/academy-ch10-group/symbols
Optional deeper verification: install Microsoft's
dotnet-symbol into a disposable tool path through the
Nexus group and request symbols for the built DLL. If your
organization disables anonymous symbol retrieval or your client/tool
version differs, preserve the HTTP/auth result and verify the stored
.snupkg instead of weakening security.
13. Rotate the API key and prove the old key stops working
Regenerate the publisher's NuGet API key in My Account. The previous
key becomes invalid. Replace NUGET_API_KEY with the new
key only after preserving the expected old-key failure against a new
synthetic version or a harmless validation attempt. Do not log
either key.
This is credential lifecycle evidence: key rotation changes authentication state, not existing package bytes.
14. Challenge: choose the correct control
Your team wants internal Learner.* packages to be
impossible to resolve from public NuGet, while ordinary Microsoft
and open-source packages still flow through Nexus. Which control is
strongest?
A robust answer uses a separate internal-only source/group or
repository-level namespace routing plus Package Source Mapping for
Learner.*. Merely listing an internal source before
nuget.org is not deterministic protection.
15. Cleanup
Delete the disposable group, proxy, and hosted repositories using supported Nexus UI/API after capturing evidence. Remove the lab publisher and disable the NuGet API-Key Realm only if it was enabled solely for this isolated instance and no other lab depends on it. Do not delete blob files or database rows manually.
unset NuGetPackageSourceCredentials_AcademyGroup NUGET_API_KEY NX_USER
printf 'After saving evidence, remove the disposable client tree: %s
' "$LAB"
Knowledge check
Why use a fresh NUGET_PACKAGES directory for the internal restore?
It prevents a previously cached package from making the restore succeed without contacting Nexus.
Where should a package be published: hosted, proxy, or group?
Hosted. Proxy mediates remote content and group aggregates reads.
What should happen when Disable redeploy is enabled and version 1.0.0 is pushed again?
The duplicate should be rejected; the failure is evidence that the release coordinate is protected from mutation.
What changes when a NuGet API key is regenerated?
The previous deployment key becomes invalid. Existing repository package bytes and metadata do not change.
What does the symbol package prove?
Only that companion debugging data exists for the build; it does not by itself prove package provenance, vulnerability status, or authorization correctness.
16. Summary
You created a complete NuGet path: public dependency through proxy, internal package through hosted, consumers through group, isolated client state, environment-backed credentials, immutable release rejection, and a version-appropriate symbol package. Lesson 3 turns those mechanics into deliberate production design choices.
Official references and version notes
- Sonatype: NuGet Repositories — hosted/proxy/group, V2/V3, authentication, Chocolatey, and symbol-server support.
- Sonatype: Configure NuGet With Nexus and NuGet CLI Usage.
- Sonatype: Realms — NuGet API-Key Realm.
- Sonatype: Tasks — current NuGet symbol-index repair task and maintenance cautions.
- Microsoft: NuGet.Config reference and Package Source Mapping.
-
Microsoft: authenticated feeds
—
NuGetPackageSourceCredentials_*environment variables. - Microsoft: dotnet nuget push and symbol packages (.snupkg).
- Microsoft: NuGet HTTPS Everywhere.
- Microsoft: .NET 10 downloads.
Version-sensitive statements were rechecked on 2026-08-26. The
mandatory lab assumes a disposable self-hosted Nexus Repository
Community Edition 3.95.2 instance, Java 21 on the Nexus side, and
.NET 10 SDK 10.0.400 on the client side. The NuGet symbol/Chocolatey
features used here were introduced in Nexus 3.95.0. Record the
actual dotnet --info and Nexus version in evidence
before execution.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.