← Start of tutorial

WIUNIX ACADEMY

MinIO after archival: a private S3 lab from pinned source

Page 2 of 4. Review the earlier steps and prerequisites before continuing.

YOUR SYSTEM. YOUR STEPS.

Where are you working?

Choose your OS and version. The prerequisites, commands and troubleshooting follow your environment.

Ubuntu26.04 LTSReviewed against official documentation ·

USE YOUR OWN VALUES

Configure this tutorial

Set the example values for your environment; related explanations and copy panels update together. Never enter passwords, private keys or tokens. Values stay in this tab and are not sent to the server or share links. Check actual DNS, certificates and network policy before running commands.

Different IP addresses, domains or ports? Open the settings below, change the values and select “Apply to tutorial”, then copy the updated commands.

Set values for my environment6 editable values
04

LET’S BUILD

Verify archives and build frozen upstream source

The Go1.27.3 linux-amd64 toolchain checksum and both source-archive hashes were checked against actual downloads on2026-10-21. The server/client commits are pinned, not latest. Archives are extracted with the data filter, dependency integrity uses Go modules/go.sum, and builds have two-worker limits. Downloads, module cache and binaries remain only under /srv/academy-minio.

Network failure, hash mismatch, unavailable modules or compiler failure stops the process. Do not replace a failed URL with an unverified mirror or cached image. Hashes establish reviewed input identity, not absence of vulnerabilities or vendor support. A DEVELOPMENT version label is possible with a raw source build; the commit and build inputs identify it.

bash

Inside the disposable container · terminal

set -eu
python3 - <<'PY'
import os,hashlib,tarfile,urllib.request,subprocess
from pathlib import Path
r=Path('/srv/academy-minio')
items=[
 ('go','https://go.dev/dl/go1.27.3.linux-amd64.tar.gz','65efbc80fdc1fa2cd2631cad5b481777a68e8725f34e89f50fc6ac714e379b40'),
 ('server','https://codeload.github.com/minio/minio/tar.gz/7aac2a2c5b7c882e68c1ce017d8256be2feea27f','71794c2df26aad0cc99e8421c58b7aa2dd55969f979b0e7d1e931042e9fabcd6'),
 ('client','https://codeload.github.com/minio/mc/tar.gz/77f82e18b5401a65958f1619df6ebb994634bd88','167415edd21bc29f5360943dac64272aa5cda0a39f3070b15cfeca671c43d975')]
for name,url,sha in items:
    p=r/'downloads'/(name+'.tar.gz')
    with p.open('xb') as f, urllib.request.urlopen(url,timeout=90) as response:
        while block:=response.read(1024*1024): f.write(block)
    assert hashlib.sha256(p.read_bytes()).hexdigest()==sha, name+' hash mismatch'
    with tarfile.open(p) as t: t.extractall(r,filter='data')
    print(name,'SHA256 verified')
env={**os.environ,'PATH':str(r/'go/bin')+':'+os.environ['PATH'],
     'GOTOOLCHAIN':'local','CGO_ENABLED':'0','GOMAXPROCS':'2','GOPATH':str(r/'gopath')}
for directory,binary in [('minio-7aac2a2c5b7c882e68c1ce017d8256be2feea27f','minio'),('mc-77f82e18b5401a65958f1619df6ebb994634bd88','mc')]:
    target=r/'bin'/binary
    assert not target.exists() and not target.is_symlink()
    subprocess.run(['go','build','-p','2','-trimpath','-o',str(target),'.'],cwd=r/directory,env=env,check=True)
    print(binary,'built from pinned archived source')
PY
How do I know it worked?

Three verified archives; minio and mc binaries built without installing host services.

05

LET’S BUILD

Create private credentials and client-scoped TLS trust

Generate unrelated random root/application keys into0600 files; no secret is printed or placed in argv. Each mc alias is written directly to its private config file. Do not use mc alias set with keys in a command, shell tracing or JSON debug output. Never put these files in Git, source packages, tickets or browser storage.

The certificate is self-signed, valid for two days and covers only127.0.0.1. It is trusted explicitly by these lab clients; the system trust store is untouched. The private-key file and full directory tree are sensitive. The command block starts only once: rerunning fails on exclusive credential files before certificate creation. This is not public ACME issuance or renewal.

bash

Inside the disposable container · terminal

set -eu
python3 - <<'PY'
import os,json,secrets
from pathlib import Path
r=Path('/srv/academy-minio')
def private(path,content):
    fd=os.open(path,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
    with os.fdopen(fd,'w',encoding='utf-8') as f: f.write(content)
for role in ('root','app'):
    user=secrets.token_hex(12); password=secrets.token_urlsafe(36)
    private(r/'private'/(role+'-user'),user)
    private(r/'private'/(role+'-password'),password)
    cfg={'version':'10','aliases':{'lab':{'url':'https://127.0.0.1:19000','accessKey':user,'secretKey':password,'api':'S3v4','path':'on'}}}
    private(r/(role+'-client')/'config.json',json.dumps(cfg))
print('Random credentials written to exclusive0600 files; none printed')
PY
umask 077
openssl req -x509 -newkey rsa:3072 -sha256 -nodes -days 2   -subj '/CN=academy-minio-loopback'   -addext 'subjectAltName=IP:127.0.0.1'   -keyout '/srv/academy-minio/certs/private.key'   -out '/srv/academy-minio/certs/public.crt'
python3 - <<'PY'
from pathlib import Path
r=Path('/srv/academy-minio')
for role in ('root','app'):
    p=r/(role+'-client')/'certs'/'CAs';p.mkdir(parents=True,mode=0o700)
    with (p/'lab.crt').open('xb') as f: f.write((r/'certs/public.crt').read_bytes())
print('Client-scoped certificate trust only; system CA store unchanged')
PY
How do I know it worked?

Protected files exist; no credentials appear in terminal output; trust is limited to the lab certificate.

06

LET’S BUILD

Write the isolated server launcher

Open the precreated empty file, paste the next complete Python file and save. It reads root credentials through MinIO _FILE variables, binds both interfaces to loopback and records only this child PID. No systemd unit or automatic restart is installed.

bash

Inside the disposable container · terminal

set -eu
nano '/srv/academy-minio/start.py'
How do I know it worked?

Saved launcher targets /srv/academy-minio and two distinct loopback ports.

Protected-file credentials and separate listeners

File content only. The API is at19000, the console at19001. Keep the API endpoint in S3 clients; a console URL is not an S3 endpoint.

python · /srv/academy-minio/start.py

Inside the disposable container · terminal

import os,subprocess,socket,ssl,time,urllib.request
from pathlib import Path
r=Path('/srv/academy-minio')
assert not (r/'run/server.pid').exists()
for port in (19000,19001):
    with socket.socket() as s: s.bind(('127.0.0.1',port))
env={**os.environ,'MINIO_ROOT_USER_FILE':str(r/'private/root-user'),
     'MINIO_ROOT_PASSWORD_FILE':str(r/'private/root-password'),
     'MINIO_BROWSER_REDIRECT_URL':'https://127.0.0.1:19001',
     'MINIO_UPDATE':'off'}
# Never enable shell tracing, dump env, or print credentials.
with (r/'run/server.log').open('xb') as log:
    p=subprocess.Popen([str(r/'bin/minio'),'server',str(r/'data'),
        '--address','127.0.0.1:19000','--console-address','127.0.0.1:19001',
        '--certs-dir',str(r/'certs')],env=env,stdin=subprocess.DEVNULL,
        stdout=log,stderr=log,start_new_session=True)
with (r/'run/server.pid').open('x',encoding='utf-8') as f: f.write(str(p.pid))
ctx=ssl.create_default_context(cafile=str(r/'certs/public.crt'))
for attempt in range(60):
    if p.poll() is not None: raise RuntimeError('Server exited; inspect private log locally')
    try:
        with urllib.request.urlopen('https://127.0.0.1:19000/minio/health/ready',context=ctx,timeout=2) as q:
            assert q.status==200
        break
    except OSError: time.sleep(1)
else: raise RuntimeError('Ready probe timed out')
print('S3 API ready with verified TLS; separate loopback console')
07

LET’S BUILD

Start the child and verify trusted readiness

Run inside the same container. The launcher refuses an existing PID file or occupied port. Failed readiness is a failure to inspect, not a reason to start a second server. Logs remain private. Readiness200 proves this process can answer, not disk durability, S3 authorization or HA.

bash

Inside the disposable container · terminal

set -eu
python3 '/srv/academy-minio/start.py'
curl --fail --cacert '/srv/academy-minio/certs/public.crt' \
+  'https://127.0.0.1:19000/minio/health/ready'
How do I know it worked?

Trusted TLS readiness200 from127.0.0.1:19000; console remains separate.

08

LET’S BUILD

Create a scoped provisioning program

Paste the following complete Go file into the precreated empty editor file. It uses official madmin-go and minio-go versions already locked in the server module. Root credentials stay in process memory; the application access key is never an argv argument or diagnostic line.

bash

Inside the disposable container · terminal

set -eu
nano '/srv/academy-minio/provision.go'
How do I know it worked?

Complete provision.go saved; no credential literals inserted.

One private bucket policy, no delete or admin grant

This application identity may list academy-objects and get/put its objects. It has no global bucket-list, deletion, historical-version, IAM or admin grant. academy-objects-outside is a negative-test bucket. Bucket existence and account checks prevent changing preexisting fixture identities; this program is not idempotent provisioning.

go · /srv/academy-minio/provision.go

Inside the disposable container · terminal

package main

import (
 "context"
 "crypto/tls"
 "crypto/x509"
 "encoding/json"
 "fmt"
 "net/http"
 "os"
 "path/filepath"

 "github.com/minio/madmin-go/v3"
 "github.com/minio/minio-go/v7"
 "github.com/minio/minio-go/v7/pkg/credentials"
)

func must(err error) { if err != nil { panic(err) } }
func main() {
 r := "/srv/academy-minio"
 read := func(n string) string { b,e:=os.ReadFile(filepath.Join(r,n));must(e);return string(b) }
 roots:=x509.NewCertPool()
 if !roots.AppendCertsFromPEM([]byte(read("certs/public.crt"))) { panic("CA rejected") }
 transport:=&http.Transport{TLSClientConfig:&tls.Config{RootCAs:roots,MinVersion:tls.VersionTLS12}}
 endpoint:="127.0.0.1:19000"
 root,secret:=read("private/root-user"),read("private/root-password")
 app,appSecret:=read("private/app-user"),read("private/app-password")
 admin,e:=madmin.New(endpoint,root,secret,true);must(e);admin.SetCustomTransport(transport)
 s3,e:=minio.New(endpoint,&minio.Options{Creds:credentials.NewStaticV4(root,secret,""),Secure:true,Transport:transport});must(e)
 ctx:=context.Background()
 // Refuse existing names rather than changing an existing bucket or account.
 for _,bucket:=range []string{"academy-objects","academy-objects-outside"} {
  exists,e:=s3.BucketExists(ctx,bucket);must(e);if exists { panic("Bucket exists; stop") }
 }
 users,e:=admin.ListUsers(ctx);must(e);if _,ok:=users[app];ok {panic("Account exists; stop")}
 for _,bucket:=range []string{"academy-objects","academy-objects-outside"} {must(s3.MakeBucket(ctx,bucket,minio.MakeBucketOptions{}))}
 policy:=map[string]interface{}{"Version":"2012-10-17","Statement":[]interface{}{
  map[string]interface{}{"Effect":"Allow","Action":[]string{"s3:ListBucket","s3:GetBucketLocation"},"Resource":[]string{"arn:aws:s3:::academy-objects"}},
  map[string]interface{}{"Effect":"Allow","Action":[]string{"s3:PutObject","s3:GetObject"},"Resource":[]string{"arn:aws:s3:::academy-objects/*"}},
 }}
 body,e:=json.Marshal(policy);must(e)
 must(admin.AddCannedPolicy(ctx,"academy-objects-rw",body))
 must(admin.AddUser(ctx,app,appSecret))
 _,e=admin.AttachPolicy(ctx,madmin.PolicyAssociationReq{User:app,Policies:[]string{"academy-objects-rw"}});must(e)
 fmt.Println("Two new private buckets and one bucket-scoped application identity created")
}
Explore all tutorials ↗