Chapter 10Lesson 02190–250 min

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.

Hosted/proxy/group.NET 10NuGet API-KeyFresh cacheSymbols

Learning objectives

  • Create disposable NuGet hosted/proxy/group repositories and explain every state change.
  • Use an isolated NuGet.Config and NUGET_PACKAGES directory.
  • 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 .snupkg on 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?

Where should a package be published: hosted, proxy, or group?

What should happen when Disable redeploy is enabled and version 1.0.0 is pushed again?

What changes when a NuGet API key is regenerated?

What does the symbol package prove?

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

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.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.