Skip to content

feat(pam): add Oracle DB access support#192

Open
saifsmailbox98 wants to merge 21 commits intomainfrom
saif/eng-4890-add-support-for-oracle-db-access-in-pam
Open

feat(pam): add Oracle DB access support#192
saifsmailbox98 wants to merge 21 commits intomainfrom
saif/eng-4890-add-support-for-oracle-db-access-in-pam

Conversation

@saifsmailbox98
Copy link
Copy Markdown
Contributor

@saifsmailbox98 saifsmailbox98 commented Apr 22, 2026

Description 📣

Adds Oracle as a PAM database resource

Infisical/infisical#6134

Type ✨

  • Bug fix
  • New feature
  • Improvement
  • Breaking change
  • Documentation

Tests 🛠️

# Here's some code block to paste some code snippets

Adds Oracle DB as the 8th PAM handler. Gateway accepts client
connections with a placeholder password, proxies pre-auth bytes
verbatim to upstream, intercepts at O5Logon to swap client-supplied
placeholder-keyed material for real-password-keyed material, and
byte-relays post-auth. Credential injection works end-to-end for
JDBC thin clients (sqlcl, SQL Developer, DBeaver) and go-ora;
user never sees real Oracle credentials.

Handler lives in packages/pam/handlers/oracle/. Ports crypto
primitives (PBKDF2+SHA512, AES-CBC session-key encryption, PKCS5
padding) and TTC codec (compressed ints, CLR byte strings, KVP
encoding) from MIT-licensed sijms/go-ora. See ATTRIBUTION.md and
ORACLE_PAM_NOTES.md for architecture notes and handoff.

Known dead code remains in this commit from the earlier full-
impersonation attempt (ano.go, nego.go, nego_templates.go, parts
of o5logon*.go, upstream.go, handshake_test.go). Kept intact to
preserve history of the approach; cleanup follows as a separate
commit.
The initial attempt used server-side Oracle impersonation (see prior
commit). That design worked through authentication but hit a
state-mismatch problem post-auth: upstream (via go-ora) and client
negotiated different TTC capabilities, so relayed queries were
rejected as protocol violations.

The replacement — proxied-auth — is already in proxy_auth.go and is
the flow wired to HandleConnection. This commit removes the dead
files and vestigial symbols that supported the old path:

Removed entirely:
  - ano.go, nego.go, nego_templates.go (pre-auth TNS/TTC negotiation
    handlers; pre-auth is now forwarded verbatim to upstream)
  - upstream.go (go-ora-based upstream dial + KVP extraction;
    replaced by dialUpstreamRaw in proxy_auth.go)
  - handshake_test.go (tested the impersonation path, orphaned)

Pruned:
  - proxy.go: handleConnectionLegacy (~200 LOC)
  - o5logon.go: O5LogonServerState, NewO5LogonServerState,
    VerifyClientPassword, deriveKey11g, md5Hash, parseIntVal
  - o5logon_server.go: AuthPhaseOne, ParseAuthPhaseOne,
    BuildAuthPhaseOneResponse, BuildAuthPhaseTwoResponse,
    BuildAuthPhaseTwoResponseFromUpstream, RunServerO5Logon,
    dumpBytes, readUint32
  - tns.go: AcceptPacket, AcceptFromConnect, ConnectPacket,
    ParseConnectPacket, MarkerPacketBytes (we forward raw packet
    bytes rather than parse/build CONNECT or ACCEPT)

Kept: crypto primitives, DATA packet codec, TTC reader/builder,
query logger, prependedConn, error helpers — all live in the
proxied-auth flow.

Drops github.com/sijms/go-ora/v2 from go.mod — no longer imported.
Adds .idea and .vscode to .gitignore. Updates ORACLE_PAM_NOTES.md
with a current-state header; historical sections below retained
for context.

Net: ~1,600 LOC removed. The handler directory goes from 12 files
to 8. Build, fmt, and the Oracle SQL test matrix (SELECT, INSERT,
DDL, PL/SQL, bind vars, NLS queries) still pass against sqlcl →
gateway → AWS RDS Oracle 19c.
The TTC query extractor was logging empty strings for OALL8 payloads
because tryExtractSQL's "skip 6 compressed ints then read a CLR"
heuristic didn't land on the SQL text — the OALL8 wire format has
variable-length headers that differ by client driver and bind
pattern. As a result, session recordings contained only session
headers (124 bytes) with no actual query content.

Replace structured parsing with a simple scan for the longest
printable ASCII run in the payload. In practice the SQL text is
always the longest such run. Verified with sqlcl: .enc file grows
from 124 bytes (empty) to ~880 bytes (with captured queries) for
a SELECT + COMMIT session.

This only affects content — the tap, packet demultiplexing, and
encrypted file I/O were all working correctly. Fix is localised to
tryExtractSQL.
sqlcl (and other JDBC thin clients) frequently bundle a piggybacked OCLOSE
for the previous cursor with the next OALL8 query in a single TTC packet.
The previous parser checked byte 0 for 0x03 (function call) and bailed when
it saw 0x11 (piggyback marker), missing the OALL8 underneath.

Scan the payload for the function-call + OALL8 byte pair instead, so the
parser finds the query regardless of any preceding piggyback prefix. Same
treatment for COMMIT and ROLLBACK, which also get piggybacked.
Matches the other SQL handlers' pattern: the client speaks plain TCP to
our local listener, we do TLS to the upstream database and translate in
the middle. No change to the client-facing UX — the same JDBC URL that
works against a plain-TCP Oracle resource now also works against a
TCPS-enabled one.

Implementing this correctly required discovering Oracle TCPS's two-handshake
flow (by reading go-ora's network/session.go readPacket RESEND branch —
credit there):

1. Dial TCP, do an initial TLS handshake. Forward the client's CONNECT
   through this first TLS session.
2. When the upstream RESEND packet's byte-5 flag has 0x08 set, Oracle
   expects the client to abandon the first TLS session and run a FRESH
   TLS handshake on the bare TCP socket. Server drops its first-round
   TLS state in lockstep. We mirror this via upgradeToTLS on the same
   rawConn, then continue the Oracle handshake through the new session.
3. Mask byte 5 on packets going downstream so thin clients (JDBC thin,
   python-oracledb thin) don't see the 0x08 signal and try to cast their
   local TcpNTAdapter to TcpsNTAdapter — the cast would fail because the
   client-to-proxy socket is plain TCP.
4. Accept TLS 1.0 as the floor for the upstream dial: Oracle 19c's
   second-round handshake negotiates down to 1.0 in some configurations
   (AWS RDS's SSL option being one of them). The outer ALPN mTLS tunnel
   remains TLS 1.2+.

Also removes now-dead SSLRejectUnauthorized / SSLCertificate fields from
OracleProxyConfig — the shared TLSConfig built in pam-proxy.go carries
that information already.

Verified end-to-end against AWS RDS Oracle 19c (SSL option, port 2484)
with sqlcl: authentication, DDL, DML+COMMIT, PL/SQL, DBMS_OUTPUT, bind
variables, and session recording all work. Plain-TCP Oracle path is
unchanged.
Pure comment cleanup on buildOracleTLSConfig — no behavior change.
- ATTRIBUTION.md: drop reference to nego.go (deleted in the
  impersonation-era cleanup) and add the upstream TCPS two-handshake
  flow adaptation from go-ora's session.readPacket RESEND branch.
- o5logon_server.go: the file header still described the
  impersonation-era architecture where the gateway acted as an Oracle
  server and drove the O5Logon exchange. Rewrite it to describe the
  current proxied-auth role — packet-layer helpers used by the byte-
  level O5Logon translation in proxy_auth.go — and drop the reference
  to upstream.go, which was removed in 3ff9cff.
Two warnings, both from the original feat commit (4b25ec2):

- Dead self-assignment state.ServerSessKey = state.ServerSessKey on
  the phase-2 request translation path. The inline comment "no-op;
  kept for clarity" already admitted it was pointless. Delete.

- Unreachable _ = prefix after a return statement inside
  replaceKvpValueKeepingSize. The prefix variable was left over from
  a refactor — the new code uses oldStart / oldEnd instead — and the
  _ = prefix trick to silence "declared but not used" landed on the
  wrong side of the return. Delete both the dead prefix declaration
  and the unreachable suppression. Also drops valStart (only used
  by the dead prefix computation).

No behavior change; go vet is now clean on the oracle handler.
Two UX simplifications to the Oracle PAM access command:

1. Stop creating a per-session TNS_ADMIN directory and printing an
   `export TNS_ADMIN=...` line in the connect instructions. The
   directory only existed to hold a sqlnet.ora that set
   DISABLE_OOB=TRUE — a defence against sqlcl's out-of-band Ctrl-C
   signalling that we never actually observed breaking. If a real
   interrupt problem surfaces we can revisit with a proper fix
   instead of a per-session file dance.

2. Change ProxyPasswordPlaceholder from "infisical-pam-proxy" to
   "password". The string value is cryptographically arbitrary —
   Oracle's O5Logon needs the client and gateway to agree on SOME
   string; any works. Shorter is easier to copy-paste. The
   accompanying "not a real credential" note in the CLI output stays.
Three items that snuck in but aren't part of Oracle PAM:

1. Untrack ORACLE_PAM_NOTES.md. It's a development notes/research log
   that belongs with scratch work, not in the branch. The file stays
   on disk (uncommitted) for local reference. Its content is also
   stale since the placeholder password changed from
   "infisical-pam-proxy" to "password" in 3ee7951 without matching
   updates to the notes.

2. Revert the github.com/emirpasic/gods indirect bump from v1.12.0
   to v1.18.1 in go.mod/go.sum. This was residue from when go-ora/v2
   was temporarily added as a direct dep in the initial feat commit —
   go-ora needed the newer gods, and Go module MVS held the bumped
   version even after go-ora was removed in 3ff9cff. Running
   `go mod tidy` against current HEAD (with main's go.mod/go.sum as
   the baseline) produces no further changes, confirming nothing we
   ship actually needs v1.18.1.

.gitignore additions (.vscode, .idea) stay — reasonable hygiene that
doesn't hurt the PR and will prevent future editor-file noise.
@linear
Copy link
Copy Markdown

linear Bot commented Apr 22, 2026

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3bfa003ff8

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread packages/pam/handlers/oracle/proxy_auth.go Outdated
Comment thread packages/pam/handlers/oracle/proxy_auth.go
Comment thread packages/pam/handlers/oracle/proxy_auth.go Outdated
Comment thread packages/pam/handlers/oracle/proxy_auth.go Outdated
Comment thread packages/pam/handlers/oracle/proxy_auth.go Outdated
Comment thread packages/pam/handlers/oracle/proxy_auth.go Outdated
Comment thread packages/pam/handlers/oracle/proxy.go Outdated
…lers

- Password plaintext no longer embedded in "password mismatch" error
  (was bubbling to gateway logs via zerolog's .Err chain).
- Long Oracle passwords (≥ 96 chars) now encode correctly: replaceKVPValue
  routes AUTH_PASSWORD through TTCBuilder.PutClr so values above the
  short-form threshold emit the 0xFE chunked form instead of a truncated
  single-byte length.
- Client-supplied username rewritten to InjectUsername in phase-1 and
  phase-2 auth requests. Matches the effect of how the postgres/mysql/mssql
  handlers overwrite the startup-packet user — the client's choice becomes
  inert; upstream always looks up the configured account's verifier.
- Dead / misleading code removed: local PacketTypeResendMarker constant
  that duplicated tns.go's PacketTypeResend, package-level min() shadowing
  the Go 1.21+ builtin, the if !use32Bit branch in extractDataPayload
  where both arms assigned the same value, and the now-unused
  encodeCompressedInt helper.
Static-analysis sweep (staticcheck + manual cross-file grep) across the
oracle handler package. All removals are symbols that were defined but
never referenced anywhere:

- tns.go: markerTypeReset, markerTypeInterrupt constants.
- o5logon_server.go: TTCMsgAuthResponse, TTCMsgBreak,
  LogonModeUserAndPass, LogonModeNoNewPass constants; AuthPhaseTwo
  fields ESpeedyKey / AlterSession / ClientInfo / LogonMode (parser
  wrote them, no reader downstream); ParseAuthPhaseTwo's KVP switch
  trimmed to the two keys actually consumed (AUTH_SESSKEY,
  AUTH_PASSWORD).
- ttc.go: TTCReader.GetNullTermString(), TTCReader.SetUseBigClrChunks()
  — both uncalled.

Also added a proper attribution header to o5logon_server.go (it was
adapting go-ora's phase-2 layout + summary-object format but lacked the
same kind of header the other ported files have), and expanded
ATTRIBUTION.md to cover it plus the specific primitives borrowed by
o5logon.go.

No behavior change. go build / go vet / staticcheck clean on the package.
Exhaustive dead-export sweep via `go doc -all` (a more reliable check
than my earlier regex-based grep, which missed untyped constants).
Removed:

- VerifierType10g / VerifierType11g / VerifierType12c — defined but no
  callers; the only verifier type our code implements (18453, 12c+
  PBKDF2+SHA512) is hardcoded, the three named constants were never
  referenced. The 18453-specific comments in the code retain the
  documentation.
- ORA12660EncryptionRequired — defined but no callers. Only
  ORA1017InvalidCredentials is actually emitted.

Post-sweep: every exported symbol in the oracle handler package has a
call site. `go build` / `go vet` / `staticcheck` clean.
staticcheck's U1000 has an exemption for const blocks: if any member is
used, sibling members aren't flagged as unused even when they are. An
exhaustive sweep that enumerates every top-level identifier via
manual grep (so const-block membership is irrelevant) caught the ones
staticcheck skipped.

Removed:
- tns.go: PacketTypeAbort, PacketTypeAck, PacketTypeAttn, PacketTypeCtrl,
  PacketTypeNull. Only the 7 PacketType values we actually dispatch on
  remain.
- query_logger.go: ttcFuncOFETCH, ttcFuncOCLOSE, ttcFuncOSTMT,
  ttcFuncOLOGOFF, ttcMsgPiggyback. Only the 4 TTC opcodes the query tap
  actually looks for remain (OALL8, OCOMMIT, ORLLBK, the outer msgFunction).

Post-sweep: exhaustive enumeration finds zero unused symbols across the
package (189 candidates checked). go build / go vet / staticcheck clean.
…e banner

The banner was printing the real upstream DB username
(pamResponse.Metadata["username"]) in the connection URL, even though
the preceding "Account:" label already shows the Infisical account name.
Since the gateway now rewrites the client-supplied username in the
O5Logon exchange to the configured real user, the client can (and
should) connect using the account name — and the banner makes that
explicit.

Before:
  Resource: aws-oracledb
  Account:  admin2
  oracle://admin:password@localhost:53521/DATABASE   ← confusing

After:
  Resource: aws-oracledb
  Account:  admin2
  oracle://admin2:password@localhost:53521/DATABASE  ← matches the label

Scope: Oracle only for now. The postgres/mysql/mssql handlers also
overwrite the client username on the wire, so the same banner change
would work there, but that needs a separate verification pass per
dialect before we extend it.
@saifsmailbox98 saifsmailbox98 requested a review from x032205 April 23, 2026 17:57
Comment thread packages/pam/handlers/oracle/proxy_auth.go Outdated
Comment thread packages/pam/handlers/oracle/query_logger.go Outdated
Comment thread packages/pam/handlers/oracle/proxy.go
Comment thread packages/pam/handlers/oracle/o5logon.go Outdated
Comment thread packages/pam/handlers/oracle/constants.go Outdated
Comment thread packages/pam/handlers/oracle/o5logon_server.go Outdated
Comment thread packages/pam/handlers/oracle/proxy_auth.go
The client's CONNECT description string was forwarded unchanged, requiring
users to know the real Oracle service name. Now we rewrite SERVICE_NAME
to match InjectDatabase from the vault config, consistent with how
username and password are already injected.
The gateway no longer checks whether the client sent the placeholder
password "password". It unconditionally encrypts the real password
from the vault, regardless of what the client typed. Auth will
succeed either way since we inject the real credentials.

The placeholder password is still shown to the user in the CLI banner
and still used for key derivation in phase-1 — only the verification
check is removed.
…SE regen

AUTH_SVR_RESPONSE is encrypted with encKey, which is derived from
session keys + CSK salt — not the password. The client and Oracle
derive the same encKey, so Oracle's original proof is already valid
for the client. No need to regenerate it.

Removes translatePhase2Response, BuildSvrResponse, and the
placeholderEncKey field from ProxyAuthState.
…t drain

The post-ACCEPT supplement peek used a 3-second read deadline to detect
whether a go-ora client sent connect-data as a separate packet. This
added a fixed 3s delay for every non-go-ora client (sqlcl, JDBC thin).

Instead, check the CONNECT packet structure: if connect-data-length +
connect-data-offset exceeds the packet size, the data wasn't inline and
a supplement will follow. Track whether the RESEND handler already
consumed it; if not, do a blocking read (the supplement is guaranteed
to be in the TCP buffer since the client sent it before waiting for
a response).

Removes prependedConn, detectConnectDataSupplement, and the timeout.
…formatting

- Remove verbose comments across all files (~20% → ~2% comment rate)
- Remove per-file go-ora attribution (ATTRIBUTION.md carries the license)
- Trim ATTRIBUTION.md to just the copyright notice + MIT text
- Make InjectDatabase mandatory (error if empty, always overwrite client's SERVICE_NAME)
- Format unknown Oracle error codes as ORA-XXXXX instead of bare "ERROR"
- Clarify ProxyPasswordPlaceholder as a decoy
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants