TLS, Reverse Proxies, Context Paths, HTTP Settings, Network Boundaries, and Secure Exposure: Guided Hands-On Workflow and Core Operations
Construct a disposable loopback-only Nexus exposure with a local TLS reverse-proxy fixture, synthetic certificate, forwarded headers, a Raw hosted repository, and verifiable secure publication without exposing credentials.
Learning objectives
- Bind or constrain a disposable Nexus backend to loopback/private reachability.
- Run a portable local TLS reverse-proxy fixture with a synthetic certificate.
- Verify TLS, forwarded-header intent, UI/API URLs, and explicit rejection of plain HTTP.
- Publish and retrieve a synthetic Raw artifact through HTTPS without placing credentials in arguments.
- Capture before/after evidence that distinguishes proxy state, Nexus metadata, and blob content.
1. Lab scenario and safety boundary
This lesson builds one disposable exposure path: Nexus listens only
on loopback at 127.0.0.1:8081; a portable Python
reverse-proxy fixture listens on loopback HTTPS 8443; a
second plain-HTTP listener on 8080 rejects requests
with 403; and one Raw hosted repository receives a synthetic text
artifact through HTTPS.
Do not run this on a shared Nexus instance. Listener changes require a restart. Use only a disposable local installation. Do not import the synthetic certificate into a corporate trust store, do not expose ports 8080/8081/8443 beyond loopback, and do not place real credentials in command history or evidence files.
Version baseline (26 August 2026). The official Sonatype download and archive pages list Nexus Repository 3.95.0, build 3.95.0-07, as the current self-hosted download. Nexus Repository 3.87+ uses Java 21 and official installers include a bundled Java 21 runtime. Record the version actually running in your lab before applying any example.
2. Preflight: prove the backend and record rollback state
The lab assumes a self-hosted Community instance using the bundled Java 21 runtime. H2 is acceptable only for this small non-container learning instance; current Sonatype guidance recommends PostgreSQL for production and does not support H2 container deployments.
# POSIX/Bash. Define the data directory for YOUR disposable archive install.
export NEXUS_DATA="$HOME/nexus-lab-data"
export LAB="$HOME/nexus-ch17-lab"
mkdir -p "$LAB/evidence" "$LAB/cert"
curl -fsS http://127.0.0.1:8081/service/rest/v1/status | tee "$LAB/evidence/status-before.json"
cp "$NEXUS_DATA/etc/nexus.properties" "$LAB/evidence/nexus.properties.before"
ss -ltn 2>/dev/null | grep ':8081' | tee "$LAB/evidence/listener-before.txt" || true
On Windows, use a disposable archive installation and record the
equivalent nexus.properties file plus
Get-NetTCPConnection -LocalPort 8081. Keep the lab
directory private because later it temporarily contains a TLS
private key and an authentication file; those files are
intentionally excluded from the evidence packet.
3. Restrict Nexus to loopback
Current runtime configuration supports
application-host and
application-port overrides in the data-directory
nexus.properties. The explicit loopback binding makes
the reverse proxy the only network ingress in this single-host lab.
# Add or update these lines in $NEXUS_DATA/etc/nexus.properties.
application-host=127.0.0.1
application-port=8081
# Keep the default application context for this lab:
nexus-context-path=/
Restart the disposable Nexus service using the installation method
you already used in Chapter 2. Then verify the listener. If Nexus
does not start, restore nexus.properties.before and
inspect nexus.log; do not keep editing unrelated
settings.
curl -fsS http://127.0.0.1:8081/service/rest/v1/status
ss -ltn 2>/dev/null | grep ':8081' || netstat -an | grep '8081'
4. Generate a two-day synthetic certificate with the correct SAN
The lab hostname is nexus.lab.test. We do not modify
public DNS or the system hosts file. Instead, clients use
--resolve to map that name to loopback for each
request.
openssl req -x509 -newkey rsa:3072 -nodes -days 2 \
-keyout "$LAB/cert/nexus.lab.test.key" \
-out "$LAB/cert/nexus.lab.test.crt" \
-subj "/CN=nexus.lab.test" \
-addext "subjectAltName=DNS:nexus.lab.test"
chmod 600 "$LAB/cert/nexus.lab.test.key"
openssl x509 -in "$LAB/cert/nexus.lab.test.crt" -noout \
-subject -issuer -dates -ext subjectAltName \
| tee "$LAB/evidence/certificate-public.txt"
The certificate is self-signed and intentionally short-lived. The
client will trust exactly this certificate with
--cacert. The private key stays under
$LAB/cert and must never be copied into evidence.
5. Run the local TLS reverse-proxy fixture
This small Python fixture is deliberately limited to the academy’s tiny requests. It is not a production reverse proxy. It forwards the client-facing host and scheme to the Nexus backend and prints a concise request log so the learner can prove which headers were emitted.
cat > "$LAB/reverse_proxy.py" <<'PYPROXY'
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from http.client import HTTPConnection
import ssl, threading, sys
BACKEND_HOST, BACKEND_PORT = "127.0.0.1", 8081
TLS_HOST, TLS_PORT = "127.0.0.1", 8443
HTTP_HOST, HTTP_PORT = "127.0.0.1", 8080
CERT, KEY = sys.argv[1], sys.argv[2]
HOP = {"connection","keep-alive","proxy-authenticate","proxy-authorization",
"te","trailers","transfer-encoding","upgrade"}
class Proxy(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
def forward(self):
length = int(self.headers.get("Content-Length", "0"))
body = self.rfile.read(length) if length else None
headers = {k:v for k,v in self.headers.items() if k.lower() not in HOP}
external_host = self.headers.get("Host", "nexus.lab.test:8443")
headers["Host"] = external_host
headers["X-Forwarded-Proto"] = "https"
headers["X-Forwarded-Host"] = external_host
headers["X-Forwarded-For"] = self.client_address[0]
upstream = HTTPConnection(BACKEND_HOST, BACKEND_PORT, timeout=60)
upstream.request(self.command, self.path, body=body, headers=headers)
resp = upstream.getresponse(); data = resp.read()
self.send_response(resp.status, resp.reason)
for k,v in resp.getheaders():
if k.lower() not in HOP and k.lower() != "content-length": self.send_header(k,v)
self.send_header("Content-Length", str(len(data)))
self.end_headers()
if self.command != "HEAD": self.wfile.write(data)
print(f"PROXY_REQUEST method={self.command} path={self.path} host={external_host} "
f"forwarded_proto=https forwarded_for={self.client_address[0]}", flush=True)
upstream.close()
do_GET=do_POST=do_PUT=do_DELETE=do_HEAD=forward
def log_message(self, fmt, *args): pass
class DenyHTTP(BaseHTTPRequestHandler):
def reject(self):
data=b"Plain HTTP is disabled in this lab. Use HTTPS.\n"
self.send_response(403); self.send_header("Content-Type","text/plain")
self.send_header("Content-Length",str(len(data))); self.end_headers()
if self.command != "HEAD": self.wfile.write(data)
do_GET=do_POST=do_PUT=do_DELETE=do_HEAD=reject
def log_message(self, fmt, *args): pass
plain = ThreadingHTTPServer((HTTP_HOST, HTTP_PORT), DenyHTTP)
threading.Thread(target=plain.serve_forever, daemon=True).start()
https = ThreadingHTTPServer((TLS_HOST, TLS_PORT), Proxy)
ctx=ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER); ctx.load_cert_chain(CERT, KEY)
https.socket=ctx.wrap_socket(https.socket, server_side=True)
print(f"TLS proxy https://nexus.lab.test:{TLS_PORT} -> http://{BACKEND_HOST}:{BACKEND_PORT}", flush=True)
print(f"HTTP deny http://{HTTP_HOST}:{HTTP_PORT}", flush=True)
https.serve_forever()
PYPROXY
python3 "$LAB/reverse_proxy.py" \\
"$LAB/cert/nexus.lab.test.crt" "$LAB/cert/nexus.lab.test.key" \\
>"$LAB/evidence/proxy.log" 2>&1 &
export PROXY_PID=$!
printf 'proxy pid=%s\n' "$PROXY_PID"
For production, replace this fixture with a supported reverse proxy/load balancer, managed certificates, rate/size/time-out policy, observability, and a network rule that permits only the proxy tier to reach the Nexus backend.
6. Prove certificate identity, API reachability, and HTTP rejection
openssl s_client -connect 127.0.0.1:8443 -servername nexus.lab.test \
-CAfile "$LAB/cert/nexus.lab.test.crt" </dev/null 2>&1 \
| grep -E 'Verify return code|subject=|issuer=' \
| tee "$LAB/evidence/tls-handshake.txt"
curl --fail --show-error --cacert "$LAB/cert/nexus.lab.test.crt" \
--resolve nexus.lab.test:8443:127.0.0.1 \
https://nexus.lab.test:8443/service/rest/v1/status \
| tee "$LAB/evidence/status-through-tls.json"
curl -i http://127.0.0.1:8080/service/rest/v1/status \
| tee "$LAB/evidence/plain-http-rejected.txt"
Expected evidence: TLS verification succeeds only for the named host; the Nexus status endpoint is reachable through HTTPS; and the plain-HTTP edge returns 403. None of those tests require a repository credential.
7. Create one disposable Raw hosted repository
In the Nexus UI on the disposable instance, create
raw (hosted) named
academy-ch17-secure. Use the default file blob store,
strict content type validation, and
Allow once write policy. Create or reuse a
disposable writer identity limited to browse/read/add/edit on this
one repository. Do not use admin for publication.
This UI step changes Nexus security/repository configuration in the database. It does not change the reverse proxy certificate or OS firewall. The first upload later creates normal asset metadata plus blob bytes.
8. Prepare a temporary credential file without command-line secrets
export NX_USER='academy-ch17-publisher'
read -r -s -p 'Disposable Nexus password: ' NX_PASS; echo
export AUTH_FILE="$(mktemp)"
chmod 600 "$AUTH_FILE"
printf 'machine nexus.lab.test login %s password %s
' "$NX_USER" "$NX_PASS" > "$AUTH_FILE"
unset NX_PASS
# Do NOT tee, cat, or copy AUTH_FILE into evidence.
Because the URL host is nexus.lab.test, curl can select
this netrc entry without putting the password in the process
argument list. In production, use your approved secret injection
mechanism and short-lived credentials where supported.
9. Publish through TLS and verify bytes independently
printf '%s
' 'academy chapter 17 secure transport proof' > "$LAB/network-proof-1.0.0.txt"
sha256sum "$LAB/network-proof-1.0.0.txt" | tee "$LAB/evidence/source.sha256"
curl --fail --show-error --cacert "$LAB/cert/nexus.lab.test.crt" \
--resolve nexus.lab.test:8443:127.0.0.1 --netrc-file "$AUTH_FILE" \
--upload-file "$LAB/network-proof-1.0.0.txt" \
'https://nexus.lab.test:8443/repository/academy-ch17-secure/com/example/academy/network-proof/1.0.0/network-proof-1.0.0.txt'
curl --fail --show-error --cacert "$LAB/cert/nexus.lab.test.crt" \
--resolve nexus.lab.test:8443:127.0.0.1 --netrc-file "$AUTH_FILE" \
-o "$LAB/downloaded-network-proof-1.0.0.txt" \
'https://nexus.lab.test:8443/repository/academy-ch17-secure/com/example/academy/network-proof/1.0.0/network-proof-1.0.0.txt'
sha256sum "$LAB/downloaded-network-proof-1.0.0.txt" | tee "$LAB/evidence/download.sha256"
cmp "$LAB/network-proof-1.0.0.txt" "$LAB/downloaded-network-proof-1.0.0.txt"
A successful PUT changes repository metadata and blob content because Nexus accepted an authenticated repository write. The reverse proxy remains stateless apart from logs. SHA-256 equality proves byte equality between producer and consumer; it does not prove provenance, vulnerability safety, or signer identity.
10. Inspect after-state and forwarded-header evidence
tail -n 20 "$LAB/evidence/proxy.log" | tee "$LAB/evidence/proxy-tail.txt"
tail -n 30 "$NEXUS_DATA/log/request.log" 2>/dev/null \
| tee "$LAB/evidence/nexus-request-tail.txt" || true
# Read-only Nexus UI/API checks:
# - Browse academy-ch17-secure and locate the exact asset path.
# - Search component/assets API for repository=academy-ch17-secure.
# - Confirm Settings -> System -> Capabilities keeps HTTP Forwarded Headers enabled.
The proxy log proves which client-facing scheme/host values the fixture emitted. The Nexus request log proves Nexus served the repository request. Browse/API evidence proves the asset exists through supported Nexus state, instead of inspecting blob filenames directly.
11. Challenge: choose the control, not the symptom
You need to prevent any non-proxy host on your LAN from reaching
Nexus while keeping the public package URL unchanged. Which layer
should change first: repository privileges,
nexus-context-path, certificate SANs, or
listener/firewall reachability? Write your answer before revealing
it.
Challenge answer
Restrict the Nexus listener/firewall path so only the reverse proxy can reach the backend. Repository privileges and URL paths do not substitute for a network boundary.
12. Cleanup and rollback
Delete only the disposable repository and lab writer identity
through supported Nexus UI/API controls. Stop the proxy process.
Restore the saved nexus.properties.before only if this
lab changed your disposable instance’s binding. Remove the exact
credential and private-key files; do not perform broad filesystem
cleanup against Nexus data.
kill "$PROXY_PID" 2>/dev/null || true
rm -f "$AUTH_FILE"
rm -f "$LAB/cert/nexus.lab.test.key" "$LAB/cert/nexus.lab.test.crt"
# Restore nexus.properties.before and restart Nexus if you changed application-host/context.
# Remove academy-ch17-secure and academy-ch17-publisher with supported Nexus controls.
13. Knowledge check
Why bind the backend to loopback in this single-host lab?
It prevents clients from bypassing the reverse proxy; the edge becomes the only ingress while Nexus remains reachable locally.
Why use --cacert rather than
-k?
Because the lab is meant to prove certificate trust and hostname identity rather than disable them.
What state does the Raw PUT change?
Nexus repository/database metadata and blob content; the reverse proxy only forwards the request and logs it.
What does HTTP 403 on the lab port 8080 prove?
The intentionally insecure edge path is rejected; it does not by itself prove the backend 8081 is private, which is checked separately.
Why is application-host=127.0.0.1 not a universal
production answer?
A multi-host or load-balanced architecture may require a private interface instead; the invariant is controlled backend reachability, not always loopback.
14. Summary and next step
The lab separated public-facing TLS policy from private Nexus service state and proved both with client and server evidence. Lesson 3 broadens the design choices for real deployments, including re-encryption, multiple package formats, context paths, connectors, private administration, and certificate rotation.
Official references and version notes
- Sonatype: Run Behind a Reverse Proxy — reverse-proxy, TLS termination, forwarded-header and context-path guidance.
- Sonatype: Configuring the Runtime Environment — application host/port/context-path configuration belongs in the data directory.
- Sonatype: System Requirements — Java 21, H2 limits, PostgreSQL production recommendation, and storage requirements.
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.