Skills
Graphstudio Hyperv Keycloak
Skill
graphstudio-hyperv-keycloak. Build a Rocky Linux Hyper-V VM via unattended kickstart (no Windows admin elevation, no RHEL subscription) and install Siemens Rapidminer Graph Studio (rebrand of Altair Graph Studio, née Cambridge Semantics Anzo - internal code/paths still say "anzo" everywhere) with SSO wired to a shared Keycloak realm. Covers the OEMDRV-ISO-via-Docker recipe (COM/IMAPI2FS from PowerShell is not worth fighting), three separate kickstart traps that each look like a different failure, the install4j console-mode driver pattern with tail-anchored prompt matching, and the multi-stage Keycloak SSO debugging chain: a discoveryURI:null red herring, a redirect-uri port+slash mismatch, a "Trust All" checkbox that only covers one of two TLS code paths, and Graph Studio's own SEPARATE certificate trust store (distinct from the JVM cacerts) that is the actual fix for the second path. Also covers Altair Units Licensing machine-slot exhaustion and its Auth Token fix, deploying a Graph Lakehouse (AnzoGraph) backend via Docker instead of a second VM (including the gRPC ports a graphmart deploy needs that the web UI alone doesn't expose), and a Hyper-V Default Switch IP change that silently breaks the browser URL, the Keycloak redirect URI, and the VM's own hosts-file entry all at once. Also covers what a locked-outsysadminaccount actually requires (a full reinstall - no offline reset exists anywhere in the product) and the install4j memory-prompt trap that validates against live Hyper-V dynamic memory rather than the stated minimum. Also covers three independently-reproducible backend bugs blocking every no-code path to get real data into a graphmart (HttpSource auto-browse NPE, a missing OSGicommons-netbundle that makes FTP File Connections unusable, and a shared file-mapping-preview crash on any source type), a workingvsftpdstaging-server recipe including thepam_shells//etc/shellsFTP-login trap, the sticky per-graphmart File Staging Area binding, and why the one working ingestion path (a hand-written SPARQL/GDICONSTRUCTquery) is virtualization-only, not onboarding, in this build. Use for any Hyper-V unattended Linux install, any Graph Studio/Anzo deployment, any Graph Lakehouse/AnzoGraph connection, a locked-out admin account, connecting Graph Studio to an external REST/HTTP data source, or any "OIDC login redirects but throws an internal SSL error" investigation.
Provenance: EXERCISED end to end 2026-08-19/24 on GraphStudio-Dev (Rocky Linux 9.8, Hyper-V,
host MARTINI-GAMING), Graph Studio 6.3.2 (graph_studio_server_linux_amd64_6_3_2_r202607081222.sh),
against the shared xcelerator Keycloak realm (xc-sso-keycloak container, see tc-sso-keycloak
skill for that realm's own history). A real user (ed) logged in through the full chain with a
role assigned. Do not promote any line marked otherwise.
0. The one-paragraph version
"Rapidminer Graph Studio" is Siemens' current name for a product that shipped as Cambridge
Semantics Anzo, then Altair Graph Studio - internal package names, ontology URIs, install
paths and error strings still say anzo throughout, and the generic install docs sometimes
describe an /opt/Anzo layout that this exact build does not use (it defaults to
/opt/graphstudio). It is a Linux-only server product (RHEL/CentOS/Rocky 7.9-9.3, no Windows
build), so on a Windows host without a RHEL subscription the path is: Rocky Linux (free,
binary-compatible, no subscription) in a Hyper-V VM, unattended-installed via kickstart, then the
vendor's install4j shell installer, then Keycloak OIDC SSO wired to a shared realm. Every stage had
at least one silent-failure trap; none of them were fixed by guessing twice.
1. Building the VM: Rocky Linux via Hyper-V, zero elevation needed for the VM itself
EXERCISED. New-VM/New-VHD/Set-VMProcessor/Enable-VMTPM etc. all succeeded in a
non-elevated PowerShell session on a host where the user is in the Hyper-V Administrators
group - no UAC prompt, no sudo-equivalent needed. Gen2 VM, MicrosoftUEFICertificateAuthority
Secure Boot template (Rocky/RHEL ship a Microsoft-signed shim, boots fine under default Secure
Boot). Dev-tier sizing per Siemens' own docs: 8 vCPU / 32GB RAM / 200GB dynamic disk is the
documented minimum for a Graph Studio dev environment, not a made-up number.
What genuinely does need elevation, and is a dead end without it: Mount-VHD,
Initialize-Disk, Format-Volume - the whole Windows disk-management stack, not just Hyper-V.
Mount-VHD fails with 0x80070522 ("A required privilege is not held by the client") even though
New-VM/New-VHD (creating the file) succeed fine. Don't spend time trying to build an OEMDRV
delivery mechanism around a VHDX for this reason - see section 2.
2. Delivering the kickstart file: build the OEMDRV ISO in a throwaway Docker container, not via Windows COM
EXERCISED, after two dead ends. Anaconda auto-detects a kickstart with zero boot-arg
editing if it's named ks.cfg at the root of a device labeled OEMDRV - this is standard,
documented Anaconda/dracut behavior (confirmed against current Red Hat docs, not assumed from
memory), and works from a second virtual DVD drive on the VM, not just physical media.
Dead end 1: PowerShell's IMAPI2FS.MsftFileSystemImage COM object. The classic
"New-IsoFile"-style scripts you'll find online rely on marshaling an IStream through PowerShell's
dynamic COM dispatch, and in practice this throws (does not contain a method named 'Stat', then
'LoadFromStream') on modern PowerShell without a working Add-Type-compiled unsafe-C# shim (which
itself is unreliable under PowerShell 7's Roslyn-based Add-Type, since it doesn't cleanly forward
/unsafe the way the old CodeDom/csc.exe path did). Burned two full attempts before abandoning it.
Dead end 2: a FAT32-labeled VHDX, blocked by section 1's elevation wall (Mount-VHD needs real
admin).
What actually worked: if Docker Desktop is already on the host, build the ISO inside a
throwaway alpine container with xorriso (apk add xorriso), which is a real, well-tested ISO
tool with zero Windows-specific footguns:
MSYS_NO_PATHCONV=1 docker run --rm -v "/d/path/to/workdir:/work" alpine sh -c \
"apk add --no-cache xorriso >/dev/null 2>&1 && \
xorriso -as mkisofs -o /work/iso/OEMDRV.iso -V OEMDRV -J -R /work/oemdrv_src && \
echo ISO_BUILD_OK"
MSYS_NO_PATHCONV=1 is not optional under git-bash/MSYS. Without it, git-bash silently
rewrites the container-side path after the colon in -v host:/container too, turning /work into
something like C:/Program Files/Git/work - the container then reports /work: No such file or directory even though the host-side path is completely correct. Scope the env var to just the
docker invocation line, not a global export (a global export breaks unrelated commands like
docker build -f).
Once the OEMDRV.iso is attached and the VM is running, it must be detached before it can be
rebuilt. The Windows host holds a lock on the file while any DVD drive references it; rm/
Remove-Item on it while attached fails or hangs. Stop-VM -TurnOff -Force first, then
Set-VMDvdDrive -Path $null on both drives, then rebuild.
3. Kickstart traps: three different errors, three different fixes, none guessable from memory
EXERCISED, in order, across three boot attempts on RHEL9/Rocky9's current Anaconda
(34.25.7.14-1.el9.rocky.0.6):
%packages --nobaseis a hard parse error, not a warning.--nobasewas removed from modern kickstart syntax; the installer terminates immediately withunrecognized arguments: --nobasebefore touching the disk. Fix: bare%packageswith@^minimal-environment.pwpolicyneeds--nostrict, not--notstrict(the latter doesn't exist and producespolicy name required for pwpolicy, a confusingly-unrelated-sounding error that actually means the argument parser choked on an unrecognized flag). More importantly:pwpolicyis deprecated and unnecessary for a pure kickstart flow - password-strength policy only applies to passwords typed interactively in the GUI/TUI; a pre-hasheduser --password=... --iscryptedvalue in the kickstart is never validated against it. Simplest fix: delete the whole%anacondablock rather than trying to get its syntax right.- A package missing from the install media (e.g.
wgeton the minimal ISO, which only shipscurl) stops the automated install with an interactiveyes/noprompt - "Problems in request: missing packages: wget. Would you like to ignore this and continue with installation?"- and OEMDRV auto-detection does not auto-answer this. It only supplies the kickstart file
itself; a package-resolution conflict still blocks on a real keypress at the console. Either
drop
wgetfrom%packages(usecurl, already present) or be ready to answer the console once.
- and OEMDRV auto-detection does not auto-answer this. It only supplies the kickstart file
itself; a package-resolution conflict still blocks on a real keypress at the console. Either
drop
Validate the kickstart with the real parser before spending 10+ minutes on another boot cycle.
ksvalidator (from PyPI pykickstart) can be run in a throwaway container with zero host
pollution and catches all three of the above before they cost a boot:
MSYS_NO_PATHCONV=1 docker run --rm -v "/d/path/to/workdir:/work" python:3.12-slim sh -c \
"pip install --quiet pykickstart >/dev/null 2>&1 && ksvalidator -v RHEL9 /work/ks.cfg"
4. Verifying an unattended install actually progressed, and catching the reboot
EXERCISED, and this is where guessing from Get-VM/Get-VHD alone goes wrong.
- A dynamic VHDX's reported size can be stale while the VM holds it open.
Get-Item/Get-VHDagainst the file while the VM is running returned the untouched 4MB header size long after a real, successful package install had happened (confirmed independently by the free-space drop on the host'sD:drive, and later byGet-ChildItemon the same file afterStop-VM, which showed the true ~2.8GB). Stop the VM before trusting a host-side file-size check - or use an independent signal (host free-disk-space delta) instead of re-querying the same locked file twice and treating agreement as confirmation. - The Hyper-V host-level event log is more trustworthy than VM
State/Uptimefor detecting a guest-initiated reboot. A kickstart'srebootcommand doesn't produce a Stop/Start cycle Hyper-V reports the wayStop-VM/Start-VMdo; insteadMicrosoft-Windows-Hyper-V-Worker-Adminlogs event ID 18514, "was reset by the guest operating system", immediately followed by a fresh 18601 "successfully booted an operating system". Poll for 18514 rather than inferring a reboot happened fromUptimeresetting, which is noisier to interpret. - The VM's firmware boot order still points at the install ISO after a kickstart
reboot. Unless you detach both DVD drives before the post-install reboot fires, the VM re-enters the installer instead of booting the freshly-installed disk. Catch the 18514 event,Stop-VM -TurnOff -Force,Set-VMDvdDrive -Path $nullon both drives,Start-VM. - No screen-capture tool ships with stock Hyper-V PowerShell. When indirect signals disagree
(or before spending another 10-minute boot cycle on a guess), the fastest real diagnostic is
asking a human to glance at
Hyper-V Manager -> Connectand read the actual console text - this caught two of the three kickstart bugs in section 3 in under a minute each, versus many minutes of blind log-spelunking per bug. Notemmc.exe(Hyper-V Manager) requires a UAC-elevated launch; a computer-use/automation tool cannot click through that dialog (Windows UIPI blocks cross-integrity-level input) - a human has to approve it.
5. Installing the product itself: install4j console mode, not blind stdin piping
EXERCISED. The vendor installer is an install4j-generated shell script. Its --help/-h
output is authoritative for available flags - check it live rather than assuming install4j's
typical -q -varfile convention applies to this specific build:
-varfile [file] Use a response file
-c Run in console mode
-q Run in unattended mode
-dir [directory] In unattended mode, set the installation directory
-overwrite / -nofilefailures / -splash [title] / -alerts / -Dname=value / -h
Fully unattended -q -varfile needs the response-file's internal variable names, which aren't
guessable and weren't reverse-engineered here; -c console mode driven by a scripted interactive
session was the path actually used and is fully reliable once done correctly.
The failure mode that cost a redo: matching a prompt pattern against the WHOLE accumulated
scrollback instead of just its tail. A regex like r"1.*[Aa]gree" intended to detect the EULA's
final "type 1 to accept" prompt instead false-matched the license body's own text ("1. LICENSE
GRANT: ... License Agreement") many screens earlier, causing 1 to be sent mid-scroll instead
of at the real prompt. Match only against the last few non-empty lines of newly-arrived output,
not the full buffer:
def tail_text(acc, n_lines=4):
lines = [l for l in acc.splitlines() if l.strip() != ""]
return "\n".join(lines[-n_lines:])
# then re.search(pattern, tail_text(acc), re.IGNORECASE) - never against the raw acc
Drive it over SSH with paramiko.invoke_shell() (a real interactive channel with a pty), not a
one-shot exec_command - the installer's own prompts need to be read incrementally and answered in
turn. Read with an idle-timeout loop (keep reading while new bytes keep arriving; stop once
output goes quiet for N seconds) rather than a fixed sleep, since package extraction and the EULA
pager take wildly different amounts of time per screen.
The install directory needs to exist and be owned by the service user before running the
installer, if not running as root. This build's default install path is /opt/graphstudio
(not the /opt/Anzo the generic vendor docs describe for other Anzo-lineage builds) - /opt is
root-owned by default, so a non-root service user hits "You have no write permissions for the
directory /opt/graphstudio" and the installer loops re-asking for a directory. Fix: sudo mkdir -p /opt/graphstudio && sudo chown <serviceuser>:<serviceuser> /opt/graphstudio before launching.
If a scripted driver doesn't detect this specific error text and abort, it will silently feed all
its remaining scripted answers into the repeating "invalid directory" retry loop - add an explicit
abort guard matching "no write permissions|is invalid|Please choose another" right after the
install-dir prompt, not just after the final prompt.
firewall-cmd inside a kickstart %post block silently no-ops - %post runs inside Anaconda's
chroot before the target system's firewalld daemon is ever started, so every firewall-cmd --permanent --add-port=... call logs FirewallD is not running and does nothing, even though the
kickstart step itself reports no error. Apply firewall rules live over SSH after first boot instead,
then firewall-cmd --reload.
Starting the service: install4j-generated server launchers accept
{start|stop|run|run-redirect|status|restart|force-reload} - check the real script name via
ls/--help rather than assuming startup.sh/start.sh (this build's is literally
graphstudioServer, matching the product's own binary/package naming, not the generic docs'
Anzo-branded examples).
6. Keycloak SSO: the discoveryURI:null red herring, and the real two-trust-store bug
EXERCISED, and this is the section worth reading in full before touching a Graph Studio OIDC provider - most of the debugging time here was spent chasing a symptom, not a cause.
6.1 Two completely separate Keycloak integrations exist - know which one you need
Graph Studio has two distinct Keycloak-related configuration surfaces, easy to conflate:
- SSO login provider (Admin -> User Management -> SSO Config -> Keycloak OIDC Provider). This is purely for authenticating a browser session - client id/secret, realm, base URI, callback.
- External User Management Provider (Admin -> Servers -> Advanced Configuration -> "Altair Graph Studio Keycloak User Management Bundle"). This is for directory sync - pulling user/group lists from Keycloak's own admin API, needs a different Keycloak client with realm-management service-account permissions, and is a prerequisite only if you want Admin -> Users -> "Add Existing Directory Users" to list anything.
If the SSO provider's "Use username directly" checkbox is enabled, #2 is not required at all - a successful OIDC login auto-provisions a plain internal Graph Studio user record, no directory sync needed. Don't build out the heavier external-user-management-provider integration (which needs its own client, its own realm-management roles, and is a much bigger lift) unless you actually need group-based auto-role-assignment (section 6.5) or centrally-managed user lifecycle.
The pre-existing default config for surface #2 on a fresh install (OSGi PID
com.cambridgesemantics.anzo.usermanagement.keycloak.KeycloakUserManagementImpl) points at the
Graph Studio host's own hostname and a nonexistent anzo realm - it logs a real startup error
(Error starting keycloak user management / KeycloakBuilder.build() failure) that is completely
unrelated to the SSO login provider and safe to ignore if you're only using surface #1.
6.2 discoveryURI: null in the error log is a symptom, not a diagnosis - don't chase it directly
A broken SSO provider produces this exact error on every login-page render, regardless of which of several different underlying causes is actually in play:
o.o.s.s.s.anzo.AnzoWrappedClient - RedirectionActionBuilder was null for client:#KeycloakOidcClient#
... discoveryURI: null ...
java.lang.IllegalArgumentException: redirectAction was not valid:Optional.empty
This exact log line was produced by three unrelated root causes across three restarts:
misconfigured %anaconda-adjacent product config (n/a here, but the pattern generalizes) - in this
case specifically, first a benign "just needs a restart" false lead (restart did nothing), then a
TLS trust failure in the metadata-fetch path (section 6.4), and even after that was fixed, the
identical log line persisted for the next real bug (the redirect_uri mismatch, section 6.3) before
finally clearing. A restart is cheap - try it once, but do not treat "the log line looks the same
after a restart" as proof a fix didn't work, and don't treat "the log line is gone" as proof a fix
did work without also confirming the actual browser flow reaches Keycloak's own login page.
Before touching TLS or client config: re-open the saved provider in the admin UI and confirm every
field actually persisted (client ID, realm, base URI). This ruled out an RDF-predicate-mismatch
theory (the ontology has both a generic SSOProvider#realm/realmName and a Keycloak-specific
SSOProvider#keycloakRealm predicate, and it was plausible the form wrote to the wrong one) in one
screenshot, saving a much longer binary-journal-inspection detour. The values do save correctly;
if a fix isn't working, look at code paths downstream of storage, not the storage layer itself.
6.3 The registered redirect_uri needs to match the ADMIN port, and tolerate a stray double slash
Keycloak's own error page (Invalid parameter: redirect_uri) reveals the ground truth callback
URL by decoding the actual redirect_uri= query parameter Graph Studio built - do this instead of
assuming the callback matches whatever port the generic docs' example uses. In this build, the
callback fired on port 8946 (the Admin application's SSL port), not 8443 (the main app's SSL
port, which wasn't listening at the time of this build - it activates after the license-key
wizard) - because all testing then necessarily happened against the Admin app.
⚠ Do not read that as a standing licence blocker. It describes one moment during the install,
and it hardened into a fabricated constraint: the companion handoff wrote it up as "blocked on a
real Altair/Siemens license key (Chris's task)", a later session repeated it three times without
checking and recommended parking Graph Studio on the strength of it. Corrected 2026-08-20 by
Chris directly: Graph Studio is fully licensed and functional; the VM is powered off by choice.
This is the "an unchecked caveat is a fabricated blocker" rule with a worked example: a sentence
that was true about a moment, restated as a fact about the environment, believed because it was
specific. Check it against the live thing before repeating it.
Additionally the built URL contained a literal double slash
(https://<host>:8946//anzo_authenticate?...), likely from [CONTEXT_PATH] in the Callback URL
field resolving to an empty/root value. Register the Keycloak client's redirect URIs as a
wildcard (https://<host>:8946/*) rather than chasing the exact string - it absorbs both the
port surprise (if you register both :8443/* and :8946/* up front) and the double-slash quirk in
one move.
6.4 "Trust All" only covers ONE of at least two outbound TLS calls in the OIDC flow
The SSO provider's "Trust All" checkbox ("Trust all certificates from Oidc source") is
undocumented beyond a one-line description in the vendor docs ("controls whether all responses
from the identity provider are trusted") - in practice, enabling it fixed the discovery/metadata
fetch (org.openanzo...AnzoSecurityClients$SslDefaultResourceRetriever, used to build the
authorization-request URL and render the login-page link) but had zero effect on a later,
separate SSL failure in org.pac4j.oidc.credentials.authenticator.OidcAuthenticator.validate()
- the server-to-server token-exchange call made after Keycloak redirects back with an authorization code. Same underlying private-CA-not-trusted problem, two different HTTP client code paths inside the same product, only one of them gated by that checkbox. Applies without a server restart (config re-read live); a restart is not needed to test whether checking it helped.
Diagnosing which TLS layer is actually broken, in order of cost:
openssl s_client -connect <host>:<port> -servername <host>from the VM - cheap, shows the real chain and issuer (verify error:num=20:unable to get local issuer certificate= private/ self-signed CA, expected for an internal realm).- The exact same JRE the running server uses, via
keytool -printcert -sslserver <host>:<port> -trustcacerts -keystore <jre>/lib/security/cacerts -storepass changeit- proves whether the system JVM trust store (which you can patch directly, see below) is the blocker. - A two-line bare
HttpsURLConnectiontest compiled and run with that same JRE, zero special flags - proves default JVM trust end to end, closer to what the app's own default-config HTTP calls do thankeytoolis. - If (2) and (3) both succeed cleanly, the failure is NOT in the JVM's system trust store at
all - stop patching
cacertsand look for an application-level trust store instead (6.5).
Patching the system JVM cacerts directly is real and sometimes necessary, but do it knowing what
it does and does not cover:
JRE=$(dirname $(dirname $(readlink -f $(which java)))) # or find the exact i4j_jres path in use
sudo $JRE/bin/keytool -importcert -trustcacerts -noprompt \
-alias <name> -file <ca.crt> -keystore $JRE/lib/security/cacerts -storepass changeit
6.5 The actual fix for the token-exchange TLS failure: Graph Studio's OWN certificate trust store
This is the section worth reading before spending an hour on cacerts. Graph Studio ships a
separate, application-level trust store, entirely independent of the JVM's cacerts - patching
cacerts (however correctly, however well-verified per 6.4) has no effect on the
OidcAuthenticator.validate() token-exchange call, because that code path reads from Graph Studio's
own store instead. The vendor docs name this explicitly (Admin -> Server Administration -> Managing
Certificates -> Adding a Certificate to the Trust Store):
In the Administration application, expand the Servers menu and click Server Certificates. ... click the Trusted Certificates tab ... click Upload Certificate, browse to the certificate file ... Once the file is uploaded, restart Graph Studio to apply the change.
Upload the same root CA (not just the leaf/server cert - the CA that signed it) that fixed step 4 of
section 6.4's diagnostic ladder, then restart. This is the fix that actually cleared
OidcAuthenticator.validate()'s SSLHandshakeException: (certificate_unknown) No trusted certificate found after both the JVM-level cacerts patch AND the "Trust All" checkbox had already
been applied and neither one touched it.
General lesson, same shape as tc-sso-keycloak section 3/20 (three/four independent trust stores
for a single TC login): a TLS trust fix verified against one HTTP client (the JVM default, curl,
Node's bundled CA list, whatever) says nothing about a DIFFERENT client library used by a different
code path in the same application. Always ask "which trust store does THIS specific failing call
actually read from" before re-testing the same fix a third time.
6.6 New SSO users get "Access Denied" until a Role is assigned - and there's a real auto-assign mechanism
A successful login (Keycloak auth + token exchange both clean) can still end in Graph Studio's own
Internal Server Error / Access Denied - this is Graph Studio's own authorization layer, expected
behavior for a freshly-provisioned user, not a bug. Checking Licensed on the user is necessary
for login at all but is not sufficient for actual page access.
Auto-assigning a baseline role on first login is a real, documented mechanism - it's not a
separate toggle, it's editing the built-in role directly: "If a user account is created in Graph
Studio but no roles are assigned, that user has the permissions of the Authenticated User
role... By default, authenticated users cannot access the Graph Studio application but can access
the Hi-Res Analytics application." Add whatever baseline permission set you want every new SSO user
to have (Data Analyst/Data Scientist/etc.) directly onto the Authenticated User role
(Admin -> User Management -> Roles -> Authenticated User), and it applies to every future new user
automatically with zero per-user step. The alternative - syncing Keycloak groups and assigning a
group to a role - is real (roles.htm: "groups (or users) are added to roles") but requires the
full external-user-management-provider directory sync from section 6.1, a materially bigger lift for
the same outcome if a single flat baseline role is all that's needed.
7. Altair Units Licensing: restarts burn a fresh "Authorized Machine" slot, and the real fix is a machine-scoped Auth Token, not a bare ANYHOST code
EXERCISED, 2026-08-20, on graphstudio-dev - this is the same class of trap as Panopticon's
Altair Units licensing problem on a sibling VM the same day (independent confirmation it's a
product-wide pattern, not a one-off).
The trap: choosing Altair Units Licensing -> Managed Licensing -> "Log into Altair One" with a
username/password causes Graph Studio to register the current machine's MAC address(es) as a new
row in Altair One's own "Authorized Machines" list on every validation, not just the first.
A normal dev loop (config tweaks, JVM heap edits, licensing experiments) restarts the service
repeatedly, and each restart mints another row bound to the same MACs. Altair One's service
appears to hard-fail once too many registrations pile up for one account in a short window - first
as SOAP timeouts (End of file or no input, SSL_connect() failed in tcp_connect()), eventually
as a flat [Altair SSO]: The credentials provided were invalid. This presents as a permanently
stuck OSGi bootstrap (all web ports 404, boot log going completely silent ~3-3.5 minutes in) that
looks identical to an external Altair outage on first inspection - it isn't; it's self-inflicted by
the restart loop.
The fix is not a bare/unscoped ANYHOST auth code. A hand-generated ANYHOST-type token from the portal was tried first and failed with the same "credentials provided were invalid" error, because Graph Studio's licensing UI submits the machine's MAC alongside the code, and the code needs to correspond to an already-registered row for that exact machine - it isn't machine-agnostic the way the name suggests.
Real working recipe:
- Let Graph Studio create its own Authorized Machines row the normal way - the first-ever Altair One username+password login does this automatically.
- On the Altair One portal, open Authorized Machines, find the row matching this machine (by hostname/MAC - not necessarily the row from the login that triggered it), check its box, click Generate Auth Token. This mints a token scoped to that machine's already-registered MACs, distinct from an unscoped ANYHOST code.
- In Graph Studio: System Administration -> Servers -> Licensing -> Altair Units Licensing -> Managed Licensing -> Use Auth Code -> paste the token -> Save.
- Restart (
graphstudioServer restart) and confirm a cleanAll Currently Registered Services startedline with no repeated Altair/SOAP errors.
Confirmed on graphstudio-dev: Authorized Machines row anzo/graphstudio-dev (both NIC MACs),
token suffix 50sj..._001, generated 2026-08-20 22:07 UTC. Once switched to Auth Code mode, a
plain graphstudioServer restart does not mint a new Authorized Machines row - confirmed by
two clean restarts afterward adding zero new portal entries. That's what actually stops the
slot-exhaustion cycle; the username/password mode is the thing to avoid for anything but the very
first login.
A plausible-looking detour that turned out to be unnecessary, worth naming so it isn't repeated:
the real OSGi ConfigAdmin property names behind this screen -
com.cambridgesemantics.anzo.licensemanager.isAuthCodeAvailable and ...altairAuthCode - are
genuine (found by string-scanning com.cambridgesemantics.anzo.licensemanager_*.jar's class
constants, not guessed) and match what the Admin UI itself writes on Save. It's possible to hand-
patch them directly in the serialized org.eclipse.equinox.internal.cm.ConfigurationDictionary
files under Server/data/osgi/configAdmin/data<pid>.pid.N - stop the service first, use the
exact same JRE the server runs (ps aux | grep AnzoLauncher to find its path) plus the real
org.eclipse.equinox.cm_*.jar on the classpath for a faithful deserialize/mutate/reserialize via
jshell, and check each property's actual runtime class before writing (this store mixes real
java.lang.Booleans with plain "true"/"false" Strings for what look like identical boolean
flags - writing a real Boolean where the app expects a String silently produces a value that
fails an .equals("true") check while looking correct in every other respect). But this was a dead
end here: the token available at the time was scoped to the wrong machine (a different Windows
host, not this VM), so the edit had no effect once the Admin UI's own next Save rotated the PID's
generation counter past it anyway. The GUI path with a correctly machine-scoped token is what
actually fixed it. Keep the property names on file for reading back what's currently persisted
during diagnosis; do the writing through the Admin UI, not the binary store.
8. Connecting Graph Studio to a real Graph Lakehouse: Docker beats a second VM, and the ports that actually matter
EXERCISED, 2026-08-21. Deploying a graphmart (needed even for a bare ad-hoc query - see 7's sibling finding that Dry Run itself is gated on the graphmart being deployed) requires a real Altair Graph Lakehouse (AnzoGraph) backend registered in Admin -> Connections. There is no embedded/local fallback - confirmed from Altair's own docs: "The Graph Lakehouse is the massively parallel processing engine that powers every knowledge graph deployment."
Don't build a second Hyper-V VM for this - a Docker container is the documented, supported path and is dramatically cheaper. Altair's own docs (Plan & Deploy -> Container Image Deployments) cover Docker/Podman/Rancher as a first-class single-server deployment target, right alongside the Enterprise-Linux-9-VM path this skill uses for Graph Studio itself. If Docker Desktop is already on the host (it was here, already running other unrelated containers), this is minutes instead of the hour-plus a fresh kickstart VM build costs:
docker pull cambridgesemantics/anzograph:latest
docker run -d -p 18080:8080 -p 18443:8443 -p 15600:5600 -p 15700:5700 `
-v "D:\GraphLakehouse\shared-files:/opt/shared-files" `
--name=anzograph --memory=10g cambridgesemantics/anzograph:latest
Default login admin / Passw0rd1. RAM: Altair states 8 GB minimum / 16 GB recommended for the
container path (lower than the 16 GB minimum quoted for a bare-VM AnzoGraph install) - 10 GB was
plenty here, confirmed by a real AnzoGraph database started log line and clean HTTP 200.
The trap that actually cost the most time here: the web UI ports are not the only ports Graph
Studio needs. docker run with only -p 18080:8080 -p 18443:8443 (the Jetty web console) looks
complete - the console itself works fine, license info loads, everything responds. But Graph
Studio's own "Create Altair Graph Lakehouse" connection form in Admin needs two more ports it
gets from a separate, easy-to-miss "Advanced" tab, not the main form: a Management Port
(gRPC), default 5600, and a SPARQL Port (gRPC), default 5700. Registering the connection with
only the container's web ports published produces a misleading error - Connection Failed: Error authenticating with Altair Graph Lakehouse Server: grpc://<host>:5700 - that reads exactly
like a wrong password, not like "nothing is listening on that port because it was never published
from the container." Confirmed via docker port anzograph: with only the web ports mapped, 5600
and 5700 simply aren't there. Fix: publish them too (-p 15600:5600 -p 15700:5700 here, remapped
off their defaults for the same reason the web ports were - avoid clashing with anything else
already bound to 5600/5700 on a host running many other services) and use the remapped numbers in
the connection form's Advanced tab, exactly the same pattern as the web HTTP/HTTPS ports.
The connection form also wants a separate "Query User" / "Query Password", and its default value
is a real but effectively unusable account. The AnzoGraph image ships a genuine query entry in
/opt/anzograph/config/passwd (confirmed: admin lives in mgrpasswd, query lives in passwd
- two different files for two different auth planes), but its password is not documented anywhere
public (checked Altair's docs, Docker Hub's own page, and the container's own credential-seeding
scripts, which only populate
ui_query_user/grpc_query_userfrom files under/k8s-activation-properties/- a directory that doesn't exist at all in a plaindocker rundeployment, so those variables stay empty and the account's password is whatever shipped baked into the image).azgpasswdgenuinely validates the current password before allowing a change (tested: a wrong guess is rejected outright, not silently accepted), so there is no cheap reset without already knowing it. Practical workaround that worked: just reuseadmin/Passw0rd1in the Query User/Password fields too.adminhas at least as much privilege as a restricted query account would need, and the form only validates that the credentials authenticate successfully, not that they belong to a distinct identity.
Changing the AnzoGraph admin UI-login password via azgpasswd writes the file but has NO
EFFECT on the actual web console login, even after a full container restart - a real, controlled
negative result, not a guess: azgpasswd /opt/anzograph/config/mgrpasswd -u admin -p admin -o Passw0rd1 returned exit 0 with no error, the file changed, the container was fully restarted
(AnzoGraph database started again in the logs), and admin/admin still failed with 401 while
admin/Passw0rd1 kept working throughout. The Jetty web console's pac4j-based login evidently
reads from a different store than the file the product's own native CLI tool writes to. Don't
chase this further without a decompile - the GUI's own Roles/password-management React component
exists in the shipped JS bundle (found by string-scanning assets/index-*.js for
validatePasswordsMatch/renderLogin) but isn't routed anywhere in this build - every nav path
was checked (/, /manage, /query, /settings and every one of their sub-tabs) and none exposed
it. This is the "AnzoGraph DB For Docker Free 8GB License" tier specifically (confirmed via
/manage's own "License Status" line) - the feature may simply not be licensed on this tier.
9. A Hyper-V "Default Switch" IP change breaks three unrelated things at once, and none of the errors say so
EXERCISED, 2026-08-21 - this is the single most expensive class of failure hit in this whole skill's history, because each symptom looks like an unrelated new bug.
The fact: Hyper-V's Default Switch (used for graphstudio-dev's primary, DHCP-assigned
NIC, eth0) can silently renumber its own host-side gateway IP with no warning and no log a normal
session would see. Observed directly three times in one sitting: 172.27.144.1 ->
172.19.176.1 -> 172.21.160.1. The guest's own DHCP client handled its side fine every time
(eth0 picked up a fresh lease in the new range automatically) - the guest was never actually
broken. Every failure that followed was stale configuration pointing at the old IP, in three
unrelated places that all had to be found independently because none of the error messages mention
networking at all:
- The browser URL (
https://172.27.152.170:8946/) - just stops loading. Obvious once you think to check, but nothing tells you the IP moved. - The Keycloak client's redirect URIs (
https://172.27.152.170:8946/*etc, registered per section 6.3) - produces "invalid redirect uri" on the Keycloak side, which reads like a configuration mistake in the client registration, not a stale IP. - The VM's own
/etc/hostsentry forsso.xcelerator.local(added in section 6, originally pointed at the same Default Switch gateway IP because that's what the host resolves to from inside the VM) - producesjava.net.SocketTimeoutException: Connect timed outdeep in Graph Studio's own server-side OIDC token-exchange call, which reads like a server-side Java bug or a firewall problem, not like a one-line hosts-file fix from section 6 having gone stale.
All three had the same root cause and the same fix shape (re-point at the current IP), but nothing
about any single error message says "check the other two as well" - each was found by directly
testing the assumption (curl to the Keycloak realm URL from inside the VM; grepping /etc/hosts)
rather than reasoning from the symptom.
Re-patching the /etc/hosts entry to the current Default Switch gateway (the first fix tried) is
not durable - it broke again within the same session, a third time, proving the point rather than
just theorizing it. The actual durable fix, applied and confirmed on the third occurrence:
graphstudio-dev already has a second NIC (eth1, added in section "connecting to Teamcenter"
work, on the separate TM_HYPERV_NAT switch) with a static IP, 192.168.222.101, that has not
moved once across this entire skill's history. The host itself also has a stable IP on that same
switch, 192.168.222.1 - and since Keycloak runs as a Docker container on the host (same as the
Graph Lakehouse container in section 8), it's reachable there too, confirmed with a direct curl.
Point /etc/hosts's sso.xcelerator.local entry at 192.168.222.1, not at whatever Default
Switch's gateway currently is - this stopped being a moving target once made to depend on the
stable switch instead. Do the same for every one of the three stale pointers above (browser
bookmark, Keycloak redirect URIs, and - for the Graph Lakehouse work in section 8 - the Docker
container's host address): move them all onto the stable subnet instead of Default Switch's DHCP
range. Prefer the static NAT switch's addresses over Default Switch's for anything durable -
browser bookmarks, OAuth redirect URIs, hosts-file entries, container connection strings - and
reserve Default Switch/DHCP for whatever genuinely needs outbound internet only.
A same-symptom trap worth flagging separately: a stale CSRF token on Graph Studio's local
(non-SSO) admin login form, producing Invalid CSRF Token '...' was found on the request parameter
- hit immediately after a failed Keycloak SSO attempt in the same browser tab. Not network-related at all; a hard refresh or a fresh incognito window cleared it. Worth ruling out before assuming a fresh networking problem when an error shows up right after switching login paths in one tab.
10. Locked out of sysadmin: no offline reset exists, and a full reinstall is the real fix
EXERCISED, 2026-08-24 - after a working sysadmin login was lost (exact cause unclear - the
password was set once during the original scripted install session and never verified or recorded
again afterward), this section covers what was actually checked before concluding a reinstall was
necessary, and the reinstall itself.
What was actually checked before giving up on recovery - worth listing so a future session
doesn't re-walk the same dead ends: the shipped install docs (confirms the username is fixed as
sysadmin, cannot be changed - "Do not change the System User ID. It must be sysadmin"); the
install4j installation.log and response.varfile (no password ever echoed to either, by design);
every firstbootFiles/*.properties file (none seed a sysadmin credential); every bundled plugin
jar searched by class name for resetpassword/resetadmin/passwordreset/createadmin (zero
hits - genuinely absent, not just undocumented); the gs CLI and the lakehouse CLI (gs help
lists no user/password/security subcommand at all; lakehouse is a client for the separate
Graph Lakehouse product's gRPC ports, not Graph Studio's own login); and the OSGi SSH management
console on port 8022 (org.openanzo.SSH bundle, confirmed via ConfigAdmin PID - genuinely a Felix
Gogo shell, but authenticates against the same locked credential store, not a separate
bypass). The actual credential store is data/journal/anzo.jnl, a ~600MB proprietary RDF journal -
hand-editing it was considered and rejected as too risky compared to the small, well-understood
ConfigAdmin PID files from section 7's licensing fix. A single Hyper-V checkpoint existed
(Automatic Checkpoint, dated to first VM boot) but predated the Graph Studio install entirely, so
restoring it would have meant redoing everything anyway - no shortcut there either.
The reinstall itself worked, and corrected a wrong assumption from section 5: the System
Administrator username/password is not an install4j console-mode prompt at all in this build -
console mode only asks for component selection, install directory, symlinks, and memory size.
sysadmin's password is set through a post-install first-boot web wizard
(http://<host>:8945/ per the installer's own final message), a completely different mechanism
than assumed when writing section 5. This matters because it means the credential is never at risk
of being silently mistyped by a scripted driver - a real human sets it, in a real form, and can
confirm it works immediately. Once logged in, Admin → Server Settings → Administrator → EDIT
is the genuine, documented way to change that password later - matching an Altair release note
found during this investigation about a fixed "internal user password change" flow. It only works
authenticated, though, so it is not a rescue path for the locked-out case this section opens with.
A new install-time trap, distinct from anything in section 5: the console installer's "Maximum
Memory in MB" prompt validates against currently-available RAM live, not the stated minimum.
Hit directly: the installer printed "The minimum amount currently supported is 1024 MB. 834 MB are
available" - a self-contradictory pair of numbers, since 834 < 1024 - and then rejected every
value above 834 with Value cannot be greater than 834, including the deliberately-chosen 16000.
This happens because the VM's Hyper-V dynamic memory allocation shrinks toward its floor while
mostly idle (exactly the mechanism documented in section "connecting to Teamcenter" for
GraphStudio-Dev's dynamic memory config), and the installer reads whatever Hyper-V has granted
at that instant, not the VM's real ceiling. Raising MemoryMinimum to force more headroom
requires the VM to be off first (Set-VMMemory refuses to set a minimum above the current startup
value on a running VM) - not worth doing mid-install. The actual fix, and it is the same fix
section 5 already documents: accept whatever value the installer's live ceiling allows (send it
back its own stated number, e.g. from a Value cannot be greater than (\d+) match, minus a small
margin), let the install complete, then edit graphstudioServer.vmoptions directly afterward
(-Xmx700m -> -Xmx16000m) and restart. A scripted driver that blindly resends a rejected value on
this specific prompt will loop forever - parse the actual ceiling out of the rejection message
instead of resending the same guess.
The Altair Units Licensing SOAP timeout from section 7 reappeared on the very first login
attempt of the fresh install, confirmed via the identical log signature
(AltairUnitLicenseServiceImpl - Error occured getting Altair unit licensing auth code: SOAP 1.1 fault ... "message transfer interrupted or timed out") landing within seconds of choosing "email
login" (the Option 2/username-password path) on the post-install wizard's licensing step. This is
strong evidence the machine-slot mechanism from section 7 is tied to the install, not just to
individual login attempts - a fresh install re-triggers it the same way a restart under the old
Option 2 config did. The already-issued Auth Token from section 7 was still valid and reusable
across this fresh install - entering it directly on the wizard's license screen cleared the
timeout immediately, without needing to generate a new one on the Altair One portal. Auth Tokens
appear to be scoped to the machine's registered MACs, not to a specific installation, so keep the
token from section 7 on hand rather than assuming a reinstall needs a fresh one.
The post-install wizard's "User Management" step offers a real choice - Internal / External
Keycloak / External SCIM - presented as if selecting one is exclusive. It is not: "Use Internal
User Management" here only governs how the bootstrap sysadmin account authenticates, and Keycloak
SSO for other users (section 6) is still added afterward as a completely separate step (Admin ->
User Management -> SSO Config), exactly as it was in the original install. Don't pick "Use External
Keycloak" here just because SSO is the end goal - that's for a different, later screen.
Driving this reinstall with a scripted paramiko session hit two failure shapes worth naming
generally, beyond the memory-prompt trap above: first, idle_secs-based prompt detection
(read until N seconds of silence, then assume a prompt is waiting) produced a false stop during
Unpacking JRE ..., which is slow and bursty rather than genuinely idle - fixed by checking whether
the last non-empty line actually looks like a prompt (ends in :, ?, ], >, or contains
"Enter") before treating silence as "waiting for input," and retrying with a longer wait otherwise.
Second, the same EULA-pager trap from section 5 recurred in a new shape: the 31-page EULA's body
text never falsely matched the real "I accept the agreement / Yes [1], No [2]" prompt this time
(the earlier tail-anchoring fix held), but the initial pattern written to detect it - requiring a
literal ? in the tail - didn't match either, because this build's real accept prompt has no
question mark at all. Write the acceptance-prompt pattern against the exact captured text of the
real prompt, not a guessed shape of what an accept prompt "should" look like - a pattern that's
too strict fails closed (safe, but requires a human to notice and fix it) rather than too loose
failing open (matches body text early, sends the wrong answer, corrupts the flow) - failing closed
is the right default when the two options are the actual tradeoff on offer.
11. Getting real data INTO a graphmart via GDI/HTTP source: three separate vendor bugs, and the one thing that actually worked
EXERCISED, 2026-08-24, testing a "zero-code REST connector" from Graph Studio to the
Teamcenter Whisperer gateway (http://whisperer.xcelerator.local:8760/data/rows/Item?array=1&limit=500,
no auth, User-Agent header required). Bottom line up front: no GUI/no-code path in this build
(6.3.2 DEV, banner "2026.1.2") got real data into a graphmart's persisted graph. Every no-code path
hit a genuine backend defect, not a configuration mistake. The one thing that did demonstrably
pull correctly field-mapped data was a hand-written SPARQL/GDI query - but see the caveat at the end
of this section before trusting that it reads live on every run; that specific claim was proposed
as testable and never actually differentially proven before this section was written.
11.1 The Database-Connection catalog: create the HTTP source here, works fine on its own
Admin -> Connections -> Database Connections -> New Connection -> HTTP Data Source. Just a URL,
optional user/password, optional "Enable SSO" checkbox - all left blank/unchecked for Whisperer,
since its /data layer needs no auth. This object alone is inert (holds only the base call); it
does nothing until something tries to browse or query through it, which is where all three bugs
below live.
11.2 Bug 1: any attempt to auto-browse an HttpSource's schema throws a null-locator NPE, unconditionally
Reproduced identically five separate ways: the "Add Source Data" dialog auto-fires this the
INSTANT an HttpSource connection is selected, before you touch anything else; the SOURCE row's
⋮ menu -> Regenerate; the same row's ⋮ menu -> Edit (opens the HTTP Endpoints mapping
screen, which immediately throws the same error on load); the graphmart's file-staging connection
detail view when it happens to browse an HttpSource-backed path; and a fresh "New Connection" ->
select existing connection flow started from scratch after deleting the prior broken source entry.
Every path produces the identical browser dialog:
Error: HttpSource does not support browsing
at https://<host>:8946/sdl/assets/main-CTVj1wuQ.js:3547:55
and, when actually invoked via the "Edit" flow specifically:
Error: Cannot invoke "com.cambridgesemantics.anzo.datatoolkit.DataLocator.source()"
because "locator" is null
Read this as: the "zero-code HTTP source" auto-schema-detection feature is unimplemented for
HttpSource in this build, not merely finicky. The UI offers "Add Source Data" -> pick a
connection -> auto-detect fields, but the underlying call it makes explicitly documents (in its own
error text) that HttpSource doesn't support the browse operation it's about to attempt anyway - a
straightforward null-check-missing bug, not a misconfiguration on the source side. Deleting and
recreating the source, toggling it, disabling instead of deleting, and reordering the click sequence
all reproduce the identical crash. The connection-picker dialog's up-arrow/refresh/search toolbar
icons are NOT a manual-sample-data workaround, despite looking like one - the up-arrow stays
permanently disabled even with the connection actively selected (it's a breadcrumb "go up one
folder level" control for hierarchical sources, inert for something that was never browsable to
begin with).
11.3 Bug 2: FTP File Connection is completely unusable - a real, missing OSGi dependency, confirmed from the server's own log
Chasing a workaround for 11.2 (Admin -> Connections -> File Store - a separate catalog from
Database Connections, used specifically for a graphmart's file staging area, gated by an
"Is Graphmart File Staging Area" checkbox that can only be set at file-store creation time, not
edited in afterward) led to standing up a real vsftpd server (recipe in 11.5, since it's a
reusable artifact even though this bug blocks its actual use here). Creating the FTP File Connection
itself worked - note: its Test Connection button throws a red herring, "Could not read from
sftp://host/path because it is not a file" - that's a cosmetic bug in the test button trying to
read a directory as a file, not a real auth/reachability failure; the connection still saves and the
underlying protocol genuinely works (verified independently with paramiko, both login and a real
file write/read-back). The REAL failure is server-side, once Graph Studio actually tries to use
the saved FTP connection to list a graphmart's staging folder:
Caused by: java.lang.NoClassDefFoundError: org/apache/commons/net/ftp/parser/FTPFileEntryParserFactory
at org.apache.commons.vfs2.provider.ftp.FtpFileSystemConfigBuilder.<clinit>(FtpFileSystemConfigBuilder.java:47)
Caused by: java.lang.ClassNotFoundException: org.apache.commons.net.ftp.parser.FTPFileEntryParserFactory
cannot be found by org.apache.commons.vfs2_2.10.0.csi
(full trace in /opt/graphstudio/Server/logs/graphstudio_execution_error.log, operation
UtilityServiceVFS#getFiles). This is genuine OSGi bundle isolation: the commons-vfs2 bundle's
FTP provider needs commons-net to implement the actual FTP protocol, and whatever commons-net
bundle should supply org.apache.commons.net.ftp.parser.FTPFileEntryParserFactory either isn't
deployed or isn't exporting that package. This makes the FTP File Connection type entirely
non-functional in this build for any purpose, independent of anything on our side - the
infrastructure in 11.5 is real and correctly built, it's just blocked by a missing jar inside Graph
Studio's own runtime.
11.4 Bug 3: the file field-mapping/schema-preview UI throws the SAME "not a file" crash on a file that genuinely exists - this is the real, broader defect
After 11.3, switching the graphmart's File Staging Area to a plain Local File Connection
(Base Folder on the VM's own disk, e.g. /home/anzo/graphstudio-uploads) sidestepped the FTP bug
entirely - and Graph Studio's own built-in browser upload ("Staging Area", reached via a Source
Layer's + -> Upload files, no manual VM path-wrangling needed) genuinely wrote real bytes to
disk: confirmed via SSH, the graphmart auto-creates a per-graphmart subdirectory under the staging
base folder (named after the graphmart's own URI, e.g.
http___cambridgesemantics.com_Graphmart_700f592985ad45d188fca59cab92cbda), and the uploaded file
landed there byte-for-byte identical to the source (111544 bytes, confirmed both sides).
But clicking into that file's own row to see its field mapping throws the identical error as 11.2:
Error: Could not read from "file:///home/anzo/graphstudio-uploads/<graphmart-subdir>/whisperer_item_sample.json"
because it is not a file.
on a path that demonstrably exists and is a real, correctly-sized file at that exact moment
(confirmed via ls -la on the VM in the same breath as the browser error). This is the important
correction from an earlier, narrower theory: the bug is not specific to HTTP sources or to
browsing an unreachable connection - it's in whatever shared schema/field-mapping-preview code path
handles ANY source type in this build, HTTP or file, live or already-uploaded. That's why a real,
present, correctly-written file still produces zero ontology classes after a full Deploy -> Current Configuration (Process Layers) cycle: nothing ever told the graphmart how to map the JSON's fields
into RDF, because the UI that would let you define or auto-detect that mapping crashes before
showing it, on every source type tested.
11.5 Working vsftpd recipe (blocked by 11.3 here, kept because it's a real reusable artifact)
Dedicated, unprivileged account, chrooted, allow-listed as the only FTP user, passive mode with a
fixed port range (mandatory - the VM sits behind Hyper-V's Default Switch/TM_HYPERV_NAT NAT, so
active-mode FTP's server-initiated data connection can't traverse it):
sudo useradd -m -d /srv/graphstudio-ftp -s /sbin/nologin gsftp
sudo mkdir -p /srv/graphstudio-ftp/uploads && sudo chown gsftp:gsftp /srv/graphstudio-ftp/uploads
sudo chown root:root /srv/graphstudio-ftp && sudo chmod 755 /srv/graphstudio-ftp # NOT writable by gsftp - see below
sudo dnf install -y vsftpd
The pam_shells.so trap: a dedicated FTP-only account with shell /sbin/nologin fails every
login with a flat 530 Login incorrect even with the exact right password, because
/etc/pam.d/vsftpd includes auth required pam_shells.so, which requires the account's shell to be
listed in /etc/shells - and /sbin/nologin normally isn't. Fix: echo /sbin/nologin | sudo tee -a /etc/shells. This does NOT grant the account real shell access (that's still independently
blocked by the nologin shell itself); it only satisfies PAM's validity check for the FTP auth path.
The chroot-writability trap: vsftpd refuses (or requires allow_writeable_chroot=YES, a real
hardening downgrade) if the chroot root itself is writable by the FTP user. Keep the chroot root
root:root mode 755, and make only a subdirectory (uploads/) owned by the FTP account - avoids
needing the writeable-chroot escape hatch entirely.
Key /etc/vsftpd/vsftpd.conf additions:
listen=YES
listen_ipv6=NO
chroot_local_user=YES
user_config_dir=/etc/vsftpd/user_conf # holds a per-user local_root override
userlist_deny=NO # + /etc/vsftpd/user_list containing only "gsftp"
pasv_enable=YES
pasv_min_port=21100
pasv_max_port=21110
pasv_address=192.168.222.101 # the VM's own stable TM_HYPERV_NAT IP, see section 9
Open both the control port and the full passive range in firewalld
(firewall-cmd --permanent --add-port=21/tcp --add-port=21100-21110/tcp).
Verify with a real client that actually implements the protocol, not curl - curl's bundled
libssh2 (irrelevant here, that's SFTP) is fine, but for testing reachability from inside a Docker
container, curl ftp://user:pass@host/path worked cleanly and is sufficient; what's NOT sufficient
is a bare port-open check. The decisive test done here: docker exec anzograph curl -v ftp://gsftp:...@192.168.222.101/uploads/ from inside the AnzoGraph container itself, not just
from the Windows host - confirming both sides the vendor docs require ("the location must be
accessible by both the Altair Graph Studio server, as well as the Altair Graph Lakehouse Server")
can genuinely reach it, including negotiating EPSV/passive mode across the NAT.
11.6 A graphmart's File Staging Area binding is sticky and easy to fight by accident
The first File Store connection a graphmart's upload flow resolves to (even implicitly, on the
first "Add Source Data" attempt) gets bound as that graphmart's Graphmart File Staging Area, a
setting on the graphmart's own top-level SETTINGS tab (not the layer's) - visible there as a
plain dropdown, editable at any time despite the confusing rule about File Store connections
themselves only being flaggable as staging-capable at their own creation time. Creating a new,
different File Store connection later does NOT automatically switch the graphmart to use it - it
keeps resolving through whatever was picked first (surfaced here as errors referencing ftp://...
long after an FTP connection had been abandoned for a Local one). If uploads keep referencing a
connection you didn't intend, check the graphmart's own SETTINGS tab for this field before assuming
anything else is wrong or restarting from a clean graphmart.
11.7 What did work: a hand-written SPARQL/GDI query - but "live" was asserted, not proven
Native GUI paths all failing (11.2-11.4), a manually-authored SPARQL query against the GDI's
SERVICE-clause HttpSource mechanism (docs:
2025.help.altair.com/2025.0/graphstudio/userdoc/gdi-http-source.htm) was tried instead, as a
"New Query" catalog object under the Data Layer (ADD -> New Query). Two real findings on the
query object itself:
- This "Query" catalog object's parser rejects a bare
INSERT/WITH ... INSERTSPARQL Update entirely - confirmed by the parser's own error listing its accepted top-level keywords (prefix / select / construct / ask / describe / explain / with, withwithrequiring an IRI literal, not the${targetGraph}template macro used in the vendor docs' ownINSERT-shaped examples). Those doc examples appear to target the generic Anzo SPARQL-endpoint/API execution context, not this specific in-UI query editor. - A
CONSTRUCTquery against the sameSERVICE <http://cambridgesemantics.com/services/DataToolkit>/s:HttpSourcepattern does run and return real, correctly field-mapped data - confirmed with actual Whisperer field values appearing in the result grid (tcw:owningUser "infodba (infodba)",tcw:itemId "Drawing-Micrometer-A2-Size-template", etc., matching a directcurlpull of the same endpoint). Its "More" menu offers onlyCopy Query to Clipboard/Copy CURL Command to Clipboard/View Query Explanation- no materialize/save/persist action of any kind. - Running this query does NOT write anything into the graphmart's persisted graph. After
multiple
REFRESH LAYERcycles and a fullDeploy -> Current Configuration (Process Layers), the layer's Ontology Model still showed "No classes to show" (with "hide elements with no instance data" on) - zero instance data, consistent across every check. The vendor docs for the GDI explicitly name two different usage modes - "ingest data into Graph Studio" (onboarding) vs. "create a virtual graph that accesses the source only when it is needed without ingesting the data" (virtualization) - and everything observed here (aCONSTRUCT-only query object, no materialize action, zero graph-side effect after running it) is consistent with this specific in-UI "New Query" object being virtualization-only, not an onboarding/ingest mechanism, in this build.
⚠ What was NOT verified, flagged explicitly per a direct challenge on this point during the
session: whether the query's Run action genuinely re-executes the live HTTP call against
Whisperer on every invocation, versus returning some cached/stale response. The evidence for
liveness so far is only that the returned data matches what a fresh curl independently pulled at
roughly the same time - which is consistent with a live call, but doesn't rule out a very recent
cache. A real differential test was designed but never run: temporarily edit the query's
s:url to a limit value below Whisperer's real row count (e.g. limit=2, which the gateway is
deliberately built to refuse with an error JSON rather than truncate - confirmed separately in this
session, {"error":"result_too_large", ...}) and re-run; a genuinely live call should return zero
triples (the error JSON has none of the bound field names), while a cached/stale response would
still show the original 500 rows. Do not cite the "live HTTP read" claim as confirmed without
running this test (or an equivalent one) first - as written, it's a single-sample correlation, not
a proven mechanism.
11.8 Net result
Real Teamcenter data (via Whisperer) was proven reachable and correctly field-mappable from Graph Studio's SPARQL/GDI layer, live in a query-result grid. It was not proven to land in, or be readable from, a graphmart's actual persisted/deployed graph by any method tried - three separate, independently-reproducible backend defects (11.2, 11.3, 11.4) blocked every no-code ingestion path, and the one working query mechanism appears architecturally scoped to on-demand virtualization, not ingestion, in this build. Worth filing with Altair/Siemens support as three distinct defects rather than one; each has a clean, minimal repro path documented above.
Related skills
tc-sso-keycloak (the parallel Teamcenter integration against the same class of shared-realm
problem - three/four independent trust stores, issuer-hostname strictness, wildcard vs exact
redirect URIs, "verify by reading the config back" discipline - almost every general lesson there
applies here too), diagnose-silent-failure.
Generated from skills/graphstudio-hyperv-keycloak/SKILL.md in the tc-automation-skills library, which is the canonical copy and also serves as the agent skill set for Teamcenter work.