Can you keep a secret? Integrating HashiCorp Vault with a Zend Framework Application

Why do we hash user passwords in our databases? So that if bad actors somehow gain access to our database, they will have a hard time stealing our secrets. But suppose we need to securely store other sensitive data in our database? Encrypt it. But how do we go about that? Assymetric, public/private key encryption with PGP would undoubtedly be the best way to go, but in the case at hand, it’s too impractical. Faced with this situation, I reasoned that symmetrical encryption was the next best option:  use one key for both encryption and decryption. But how to store the encryption key securely? As a plain text file sitting on the same server as the database username and password? Which, by the way, are also sitting there in plain text, if not in plain sight?

Disclosures, Disclaimers, Excuses

Now that I have your attention, a quick digression. I am not a security expert — I just try to pay attention to those who are. So please, use your own judgment at your own risk yadda yadda yadda. Moreover, I’m not even officially a web application developer. My job title has nothing to do with that. Long story short: I stumbled into coding some 20 years ago because in my workplace we could find no software, commerical or otherwise, that met our needs in managing a busy federal court interpreters office. So we (OK, I) rolled our own, and have been rolling ever since. Now I’m working on the next iteration of our great project, using Zend Framework 3 and Doctrine, and that’s the context in which this security problem arises.

Wandering back to our main topic: we need to store some sensitive data of contract court interpreters in a database, we want to encrypt it symmetrically, and we need to keep the secret encryption key secret.

Hashicorp Vault to the rescue

The very clever people at Hashicorp generously provide, among other things, a secret-management tool aptly named Vault. As their website puts it,

Vault is a tool for securely accessing secrets. A secret is anything that you want to tightly control access to, such as API keys, passwords, certificates, and more. Vault provides a unified interface to any secret, while providing tight access control and recording a detailed audit log.

The use of Vault is a complex subject, and the following discussion is no substitute for the documentation. We should just point out that secrets (yes, that’s the technical term) can be stored in several different types of storage backends, and authentication can happen by way of a number of different authentication backends. These things are identified and accessed by way of paths similar to a filesystem. And that unified interface mentioned above is an HTTP API. You talk to Vault by sending HTTP requests with an authentication token in your request headers, and get JSON responses. Of course, this makes it a snap for applications to communicate with Vault with the language of your choosing.

The challenging part is devising a good security model, and there is a vast — if not to say baffling — array of choices and decisions to be made. I’m still not convinced my current solution is the best, so it’s subject to change. But let’s step through it as it is at the moment. Of course, you’ll first need to install and configure and run Vault, and they make that pretty damn easy. (Though you do have to decide what machine(s) to put it on. There’s replication and high availability and all that sexy stuff for large-scale deployments. Ours happens to be a modest configuration, involving a small set of users behind our organization’s firewall.)

Creating Policies

In Vault, you create authentication tokens that have policies attached to them, determining what the bearer of the token is allowed to do. I want our authenticated application not to be authorized to read our ultimate secret, but to grant an auth token bound to a policy that does allow it. So we need a policy for reading the secret. We also need a policy for creating the token that’s attached to the policy for reading the secret. Sound complicated? Don’t worry: it is! But the underlying principle is a basic one:  grant access that is as permissive as necessary, and no more. So I took out my quill and wrote policies, thus:

read-secret.hcl

path "secret/data" {
  policy = "read"
}

create-token.hcl

path "auth/token/create/read-secret" {
	policy = "write"
}

Incidentally, that .hcl extension you see refers to Hashicorp Configuration Language. It’s a lot like JSON and you’ll pick it up immediately. Then, after logging into Vault with sufficient privileges, we wrote our policies to Vault via its CLI:

vault policy-write read-secret read-secret.hcl

and

vault policy-write create-token create-token.hcl

and finally, the rather more esoteric one:

vault write auth/token/roles/read-secret allowed_policies=read-secret

which magically allows the “role” to bestow access to something that the role itself cannot access.

Authenticating the application

As implied above, I decided Step One should be that not the user but the application authenticates itself against Vault using the TLS backend. To that end, I created my certificates (thank you, https://jamielinux.com/docs/openssl-certificate-authority/ for help with that), stored them in Vault,

vault write auth/cert/certs/web display_name=web policies=create-token \
	certificate=@/path/to/your/cert.pem 

and tested it out with the Vault cli,

vault auth -method=cert -client-cert=/path/to/your/cert.pem \
    -client-key=/path/to/your/key.pem

then with the HTTP API using our good friend curl. By the way, before thinking too much about integration with ZF, I followed the same procedure with all this token stuff until I was able to go from zero to reading the secret (encryption key) from the command line. I predicted (correctly!) that the heavy lifing was setting up the Vault stuff, and getting it to play well with ZF would be more like fun than work.

Getting a token for getting the secret

After the previous command, now that we’re authenticated via TLS, we can go

vault token-create -role=read-secret

and get a response something like

Key            	Value
---            	-----
token          	ef2fb0d1-1644-937f-5326-3c6270abc3ba
token_accessor 	522c0a9d-7897-a670-e511-650d37ea6d20
token_duration 	768h0m0s
token_renewable	true
token_policies 	[default read-cipher]

Getting the secret

And finally, authenticate with the token we just got:

vault auth ef2fb0d1-1644-937f-5326-3c6270abc3ba

and go for the gold

vault read secret/data

resulting in

Key             	Value
---             	-----
refresh_interval	768h0m0s
cipher          	b862600aaeba7c4ccd74006d2e616083ffb7031a3b088e743080bcf32e90f3b4

and that “cipher” you see is the encryption key for encrypting/decrypting our sensitive database fields, a step we will perform in our PHP application.

In all honesty, it’s a bit more complicated than this because we are going to be using response wrapping. When a client sends the header so indicating, the response is not the thing requested, but another auth token with which you can get the thing requested. And that token is a single-use token:  use it once and then it’s revoked. You can also limit the time-to-live on this token, so that whether it gets used or expires, it is not going to be hanging around for long as a valid token. This exemplifies the Vault (and general security) principle of limiting the exposure of a secret.

Plugging it into ZF

The front end

At last, the fun part. (Not to say that Vault isn’t loads of fun — it absolutely is! But after dealing and struggling with something unfamiliar, it’s comforting like a warm bath to return to the familiar, isn’t it?) I decided that this application would have its Vault capabilities in a separate module to make it easy to enable or disable. If it’s disabled, they simply don’t get to work with sensitive data. Let’s work from the front end to the back. The form has a good number of fields, but the ones we’re concerned with look like this:

because there’s no reason to expose this data if they don’t ask. The encrypted values are tucked away as hidden fields. When they click those lock thingies, we display a modal dialog inviting the already-authenticated user to re-authenticate (in case they load the form, wander off to the bathroom, and just then an identity thief comes up to their workstation). A couple of Javascript xhr calls later, if all goes well, the dialog goes away and these fields are re-populated with the decrypted values.

The Controller

And the above-mentioned xhr sends a POST request containing the encrypted values to /vault/decrypt, a route mapped to this action method in our module/Vault/src/Controller/VaultController.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
        public function decryptAction()
    {
        $params = $this->params()->fromPost();
 
        try  {
 
            $key = $this->vaultService->getEncryptionKey();
            $cipher = new BlockCipher(new Openssl());
            $cipher->setKey($key);
            $decrypted = [
                'ssn' => $cipher->decrypt($params['ssn']),
                'dob' => $cipher->decrypt($params['dob'])
            ];
            return new JsonModel($decrypted) ;
 
        } catch (VaultException $e) {            
             return new JsonModel(['error'=>$e->getMessage()]);
        }
    }

The part involving Vault gets our encryption key, and it’s a one-liner. The next bit simply makes use of zend-crypt to take care of decryption.

You notice $this->vaultService. That’s a dependency injected via the constructor, so there’s a factory which pulls it from the container. Boring, so we’ll skip that part. Let’s have a look at the module’s module.config.php (ooh, exciting!)

Configuration

<?php
namespace SDNY\Vault;
use Zend\Router\Http\Literal;
 
return [          
    'vault' => [           
        // override these with a local configuration
        'vault_address' => 'https://vault.example.org:8200', 
        'sslcafile'     => '/usr/share/ca-certificates/ca-chain.cert.pem',
        // these settings must match the configuration set in Vault
        'ssl_key' => '/path/to/your-private-key.key.pem',
        'ssl_cert' => '/path/to/your-cert.pem',    
        'path_to_secret' => '/path/to/your/secret', // including leading slash
        // but do not change this adapter
        'adapter'       => 'Zend\Http\Client\Adapter\Curl',       
    ],    
    'service_manager' => [
        'factories' => [
            Service\Vault::class => Service\Factory\VaultServiceFactory::class,           
        ]
    ],
    'controllers' => [
        'factories' => [           
            Controller\VaultController::class => Controller\Factory\VaultControllerFactory::class,
        ]
    ],   
];

So the module’s config sets some default/dummy values, with the expectation that they will be overridden by a local configuration file called something like config/autoload/vault.local.php, consistent with the ZF convention. No reason to put this in a public repository.

The Vault service

We have a VaultServiceFactory that injects all that config for us when it instantiates our VaultService, which is what does the talking to Vault.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
<?php
/**
 * module/Vault/src/Service/Vault.php
 */
 
namespace SDNY\Vault\Service;
 
use Zend\Http\Client;
 
 
/**
 * Extension of Zend\Http\Client for communciating with Hashicorp Vault
 * 
 * The purpose is enable us to store sensitive data in MySQL using symmetrical
 * encryption while avoiding having to store the encryption key in plain text 
 * anywhere at any time. All the configuration has to be correctly set before 
 * instantiation. Error-checking is left up to the consumer.
 * 
 */
 
use Zend\EventManager\EventManagerAwareInterface;
use Zend\EventManager\EventManagerAwareTrait;
 
class Vault extends Client implements EventManagerAwareInterface {
    use EventManagerAwareTrait;
 
    protected $events;
 
    /**
     * mapping of string keys to CURL integer constants
     * 
     * we need this because if a config array key is an integer
     * unfortunate things happen when the framework merges the configs
     * 
     * @var array
     */
    private static $curlopt_keys = [
        'ssl_key' => \CURLOPT_SSLKEY,
        'ssl_cert'=> \CURLOPT_SSLCERT,        
    ];
 
    /**
     * vault authentication token
     * 
     * @var string
     */
 
    private $token;
 
    // some more instance variables omitted for brevity
 
    /**
     * constructor
     * 
     * @param array $config
     */
    public function __construct(Array $config) {
 
        $this->vault_address = $config['vault_address'] . $this->prefix;
        $this->path_to_secret = isset($config['path_to_secret']) ? 
            $config['path_to_secret'] : null;
        $curloptions = [];
        foreach ($config as $key => $value) {
            if (key_exists($key, self::$curlopt_keys)) {
                $curloptions[self::$curlopt_keys[$key]] = $value;
            }
        }
        $config['curloptions'] = $curloptions;       
 
        parent::__construct(null, $config);
        $this->getRequest()
            ->getHeaders()
            ->addHeaderLine('Accept: application/json');
    }
 
    // some setters/getters omitted for brevity
 
 
    /**
     * checks response for errors
     * @param Array $response
     * @return boolean true if error
     */
    public function isError(Array $response) {
        return key_exists('errors',$response);
    }
 
    /**
     * resets request, response, etc, and restores
     * request header for JSON responses
     * 
     * @return \SDNY\Vault\Service\Vault
     */
    public function reset()
    {
        parent::reset();
        $this->getRequest()
            ->getHeaders()
            ->addHeaderLine('Accept: application/json');
        return $this;
    }
 
    /**
     * attempts Vault TLS authentication
     * 
     * this will attempt to authenticate using TLS certificates, which have to 
     * have been installed and set in our configuration up front. 
     * 
     * @link https://www.vaultproject.io/docs/auth/cert.html
     * 
     * @return Vault
     * @throws VaultException
     */
    public function authenticateTLSCert($options = [])
    {
        $this->setMethod('POST')
            ->setUri($this->vault_address .'/auth/cert/login')
            ->send();        
        $response = $this->responseToArray($this->getResponse()->getBody());
        if ($this->isError($response)) {
            $this->getEventManager()->trigger(__FUNCTION__, $this, []);
            throw new VaultException($response['errors'][0]);
        }
        $this->token = $response['auth']['client_token'];
 
        return $this;
    }
 
    /**
     * Attempts to acquire access token that is authorized to read the cipher 
     * we use for symmetrical encryption/decryption of sensitive Interpreter 
     * data.
     * 
     * @return Vault
     * @throws VaultException
     */
    public function requestCipherAccessToken()
    {
        $this->getRequest()->getHeaders()
                ->addHeaderLine("X-Vault-Token:$this->token")
                ->addHeaderLine("X-Vault-Wrap-TTL: 10s");
        $endpoint = $this->vault_address . '/auth/token/create/read-cipher';
        $this->getRequest()->setContent(json_encode(
               [
                // maybe reconsider these settings
                'ttl' => '5m',
                'num_uses' => 3,
               ]
        ));
        $this->setMethod('POST')->setUri($endpoint)->send();
        $response = $this->responseToArray($this->getResponse()->getBody());
        if ($this->isError($response)) {
            $this->getEventManager()->trigger(__FUNCTION__, $this, [
                'message' => 'failed to get token for cipher access'
            ]);
            throw new VaultException($response['errors'][0]);
        }        
        $this->token = $response['wrap_info']['token'];
        return $this;             
    }
 
 
    /**
     * unwraps a wrapped response and returns it as an array
     * 
     * @param string $token
     * @return array
     */
    public function unwrap()
    {
        $this->reset();
 
        $endpoint = $this->vault_address . '/sys/wrapping/unwrap';
        $this->setAuthToken($this->token);
        $this->setMethod('POST')->setUri($endpoint)->send();
 
        $response = $this->responseToArray($this->getResponse()->getBody());
        if ($this->isError($response)) {
            $this->getEventManager()->trigger(__FUNCTION__, $this, [
                'message' => 'failed to unwrap response'
            ]);
            throw new VaultException($response['errors'][0]);
        }
        if (isset($response['auth'])) {
            $this->setAuthToken($response['auth']['client_token']);
        }        
        return $response;      
    }
 
    /**
     * requests response-wrapped encryption key
     * 
     * @param string $token authentication token
     * @return Vault
     * @throws VaultException
     */
    public function requestWrappedEncryptionKey()
    {
        if (! $this->path_to_secret) {
            throw new VaultException('path to secret has to be set before calling '.__FUNCTION__);
        }
        $endpoint = $this->vault_address . $this->path_to_secret;        
        $this->getRequest()->getHeaders()->addHeaderLine("X-Vault-Wrap-TTL: 10s");
        $this->setMethod('GET')->setUri($endpoint)->send();
        $response = $this->responseToArray($this->getResponse()->getBody());
         if ($this->isError($response)) {
            $this->getEventManager()->trigger(__FUNCTION__, $this, [
                'message' => 'failed to get wrapped encryption-key response'
            ]);
            throw new VaultException($response['errors'][0]);
        }
        $this->setAuthToken($response['wrap_info']['token']);
        return $this;  
 
    }
    /**
     * gets encryption key.
     *
     * convenience method that wraps the several 
     * steps into one.
     *
     * @param string $token authentication token
     * @return string
     * @throws VaultException
     */
    public function getEncryptionKey()
    {
        $this->authenticateTLSCert()
                ->requestCipherAccessToken()
                ->unwrap();
        $this->requestWrappedEncryptionKey();
        $key = $this->unwrap()['data']['cipher'];
        return $key;
 
    }
 
    /**
     * sets Vault authentication token header
     * and instance variable
     * 
     * @param string $token
     * @return \SDNY\Service\Vault
     */
    public function setAuthToken($token)
    {
        $this->getRequest()
            ->getHeaders()
            ->addHeaderLine("X-Vault-Token:$token");
        $this->token = $token;
        return $this;
    }        
 
    /**
     * converts json to array
     * 
     * @param string $json
     * @return Array
     */
    public function responseToArray($json) {
 
        return json_decode($json,true);
    }
 
    /**
     * attempts user/password authentication
     * 
     * this will attempt to authenticate user against Vault's 
     * userpass auth backend. NOTE: looks like we won't be using this auth 
     * method after all, so this method is not currently used.
     * @link https://www.vaultproject.io/docs/auth/userpass.html
     * 
     * @param string $user
     * @param string $password
     * @return array Vault response as array
     */
    public function authenticateUser($user,$password)
    {
        $uri = $this->vault_address . "/auth/userpass/login/$user";
        $this->getRequest()->setContent(json_encode(['password'=>$password]));
        $this->setUri($uri)->setMethod('POST')->send();
 
        return $this->responseToArray($this->getResponse()->getBody());  
 
    }
 
}

This Vault service extends Zend\Http\Client because it basically is a specialized http client. The general pattern is that most of these methods attempt to get the token we need, store it in $this->token, and return $this so the lazy coder using the class can save some keystrokes.

We make use of the handy Zend\EventManager\EventManagerAwareInterface and Zend\EventManager\EventManagerAwareTrait and do a fair amount of $this->getEventManager()->trigger(). It’s most definitely a work in progress, but by the time this project is done (or pretty close) there are going to be event listeners responsible for logging things and generally paying attention. On the Vault side, I plan to (figure out a way to) monitor the audit log with the ultimate goal of trying to detect a possible breach and hit the panic button.

So we’ve demonstrated decryption, how about encryption? Coming soon! But as you can see, it will be straightforward.

Further thoughts on Vault and PHP Applications

Remember we started out with a reference to the practice of storing database credentials in plain text? Vault provides a database secret backend so you can avoid doing that. Vault also has a very straightforward user/password authentication backend that makes it tempting to consider implementing a Zend\Authentication\Adapter\AdapterInterface that uses it.

All of this is to say that in terms of security, I think Vault offers us ways to take Zend Framework applications, and PHP applications generally, to the next level.

Conclusion

As noted at the beginning, this solution to my challenge of keeping our encryption key secure may not be the best possible. But it’s a start. The first objective was to avoid leaving our secret lying around in plain text, and this accomplishes that. My holy grail is to make it so that even in a worst case scenario, where an attacker gains root access to the machine where this application is installed, my precious social security numbers and dates of birth would still not be compromised. Like anything, this model is not guaranteed bullet-proof against a breach that severe, but again, it’s a start.

Hopefully this demonstrates that once you have Vault going, it’s quite easy to integrate with your ZF application. As a personal note, I would add that this is actually my debut blogging about Zend Framework, and I do feel a little self-conscious about presuming to address an audience with so many badasses in it. Stage fright notwithstanding, I am truly interested in hearing your comments, so please feel free.

References

Fuckheads block my mail to my dad

My dad, a retired professor of musicology, is a DSL customer of the Ontario and Trumansburg Telephone Companies. We have carried on email correspondence for years, peppering our prose with F-bombs whenever the urge arises. One fine day he stopped receiving mail from my gmail account, so I suggested he contact his ISP to see if I was being blocked. It turns out that his provider had begun applying “decency filters” to his incoming mail without his knowledge or consent, and my messages had been quarantined. When he demanded that they stop inspecting his mail for “decency,” they replied with boilerplate instructions on how to whitelist my address. He got on the phone and explained that what he wanted was not to whitelist his correspondents one by one, but to have the decency filter disabled outright. The drone with whom he spoke appeared not to understand. He is escalating his case up to the telco’s CEO Paul Griswold, and copying his correspondence to the New York ACLU and the Electronic Frontier Foundation.

Grouchy old bastard that he is, my dad has little patience with mindlessness and stubborn stupidity of this sort. So in his latest round of correspondence, he busted their balls thus:

One of your people called me a short time ago, and astonished (and, I must admit) infuriated me by his real (or feigned) inability to understand what I was trying to say to him. So let me try to get at it in writing.

There is a world of difference between filtering spam as such and filtering for what you mistakenly take to be “decency.” The first is allowable; the second is abominable. And to do either or both without telling the subscriber what you are doing is detestable. If your technology is so crude that it cannot distinguish between spam and four letter words, you need new technology. But be that as it may, your minds should be capable of grasping the point.

So got it? It is really very simple.

Now, I am an old professor, so let me read you a lecture; please hold still for a few moments. The US Constitution of course has nothing directly to do with our dispute. It does, however, bear on it indirectly in a most profound way. The Constitution is not merely the legal basis of our country, it also has determined our ethos.

Now read it. You will see that it is profoundly mistrustful of the political judgments of what its authors thought of as “the mob,” a group to which you and I probably would have been thought to belong. That is why the Senate was originally elected by state legislatures; that is why even today with direct election, it remains profoundly unrepresentative. On the other hand, The Constitution in its first final form — that is with the Bill of Rights added to the original document — is deeply concerned about individual liberties. It is accordingly at once quite libertarian and somewhat undemocratic.

To leap to our little situation: not only are you not my censor, but your attempt to assume this dreadful role really does violate America’s basic ethos and is accordingly deeply offensive to people like me. Despite everything, we still believe in each citizen’s basic responsibility for himself. We refuse to turn this responsibility over to others. You have no right to take it away from for me on your own initiative. And in truth, I have no right to turn it over to you so long as I am sentient.

That your definition of “indecency” is idiotic and contrary to sound morality is another matter and could be explained to you only in the context of another little lecture. I fear you have had enough for now.

Please actually read and understand what I have written you. Do not reply with some canned nonsense from corporate headquarters or anything of that silly sort. There’s no point in that. If you cannot engage me in reasonable and intelligent discourse, do not engage me at all. But do turn off that thrice damned decency filter.

What do you make of that? What are the odds that anyone will understand what he’s saying and respond appropriately?

Ah, the Department of State

You spend numerous minutes filling out a multi-page “wizard” for your passport renewal application. You notice that when you click a checkbox somewhere, the whole page pointlessly reloads with the data in the same state. It’s annoying, but you carry on.

You notice that when your textfield input exceeds the maximum number of characters allowed, instead of just refusing to accept more characters, this form has some Javascript that erases all your input in that field, so you have to start over. Very annoying, but you carry on.

You notice an error in your input, but when you put your mouse cursor in the field to edit it, all the input disappears, so you have to start over.

By now you’d like to have a talk with the people who coded this application — even more so, with the people who tested it and declared it production-ready. But you sigh and carry on until the final page, and then…

Server Error in ‘/’ Application.
Index was outside the bounds of the array.
Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.

Exception Details: System.IndexOutOfRangeException: Index was outside the bounds of the array.

Source Error:

An unhandled exception was generated during the execution of the current web request. Information regarding the origin and location of the exception can be identified using the exception stack trace below.

Stack Trace:

[IndexOutOfRangeException: Index was outside the bounds of the array.]
WizardManagerForNewForms.processControls(Control control, String FormType) +31109
WizardManagerForNewForms.processControls(Control control, String FormType) +31063
WizardManagerForNewForms.processControls(Control control, String FormType) +31063
WizardManagerForNewForms.UpdateApplicantData(StateBag activeViewState, ControlCollection activeControls, String ApplicantSSN, String ApplicantFirstName, String FormType) +313
[etc]

Version Information: Microsoft .NET Framework Version:2.0.50727.832; ASP.NET Version:2.0.50727.832

Lovely, isn’t it? For you non-geeks out there, this type of verbose technical information is often useful to the developers of a program in the course of debugging — although usually only a couple lines of the strack trace are actually relevant to your problem (the function that dumps the stack trace doesn’t know that, so it gives you the whole thing). But you the end user do not know or understand any of this, nor should you. What you should see, if the application pukes, is a polite and vague error message apologizing for the inconvenience, while behind the scenes the technical stuff is saved in a log file or other storage. The geek-speak is just annoying and confusing mumbo-jumbo.

Displaying all the error output is not just bad form. It also leaks internal information that might be useful to a potential attacker. It should be none of our business that State is running
Microsoft .NET Framework Version:2.0.50727.832 and ASP.NET Version:2.0.50727.832 and that it bombs when it tries access an array element that does not exist.

Whether it’s a .NET issue, or an application-level bug, I gotta go. I have a paper passport renewal application to fill out.

Speakeasy now sucks

Once upon a time I was a satisfied customer of Speakeasy DSL, Internet service provider. The static-IP service was expensive but it worked. On the rare occasions when it did not, you could call them. When you called, a human would answer. And that human who answered would have a clue about computers and networks, and the problem would be addressed promptly. This level of service costs money and I was glad to pay.
Then I moved to a place two blocks from where I had lived hitherto. Speakeasy’s website said “Moving? Not a problem” and represented that you could get your existing service transferred to a new location. It turns out that was false. When I tried to move the service they said I had to enter into a brand new 12-month contract and go through the horror of installation. I agreed to this because it was take it or leave it. Installation proved to be a textbook study in ass-rape. With third parties like Verizon and Covad involved, making you take off work to wait for hours for their techs who then stand you up, or cannot complete the work and make you schedule another appointment, you know we are talking major ass-fucking. Another cute trick Covad pulled was sending a tech unannounced and calling me in the middle of the work day and saying “where are you? We can’t get in because you aren’t here.”
I grant you, Speakeasy is not Covad. But when I moved again a year later, I said no more Speakeasy. It’s expensive and the installation is a nightmare for which I simply do not have the time, let alone the patience.
I drew in a deep breath and signed up for Comcast’s TV/VOIP/Internet bundle, despite Comcast’s notoriously poor reputation for service. (By the way, the Comcast tech came punctually when they said he would, and did what they said he would, and the service has been perfectly fine so far.) I called Speakeasy on September 12 to cancel. They said I could not cancel without incurring a termination fee because the contract did not expire until the 22nd. I said, you coerced me into this contract in the first place after first saying I could move without extending it. And we are talking a mere ten days so what’s the big deal. Too bad, said the unctuous prick in so many words, call back and cancel on the 22nd or eat the fee. I called back on the 22nd and guess what. They said call back on the 23rd. Overnight, I got another invoice for $125 plus another $90 detailed simply as “service charges.”
So I called back on the 23rd and warned the guy who answered that I had been given the runaround and excuse me if I sound annoyed. He made it all right, and made the $90 go away — or so he says, we will see if Speakeasy comes after me for it.
They used to be a decent bunch — there was professionalism mixed with genuine human warmth and intelligence. It seems to me that since the Best Buy acquisition they have turned into just another shithead corporation determined to make the consumer navigate and endless voicemail maze, only to get fucked with and jerked around once they reach a human.

Linux distros, religious wars

A Beginner’ s Guide to Linux Distros provides an illuminating comparision of several of the most widely used Linux distributions. The problem, for typical non-technical windoze users looking for relief, is that their eyes will glaze over upon reading this, and they will likely be intimidated and confused by all the geek jargon.

I’ve been there, having started trying Linuxes in 1996. I have been through half a dozen different Linuxes with successively greater degrees of success (as they installs and hardware support improved). I think it’s now reached the point where there is really no reason why an average windoze user can’t learn to use a distro like Ubuntu. I don’t know if many would have the cojones to install it, but if you give them a machine with Ubuntu already on it, I can’t help but think they could be happy, or learn to be happy. Yes, Gnucash is different from Quicken, OpenOffice is different from Miscrosloth Office, and so on. Users have to want to leave the abusive relationship with windoze enough to put up with some migration pains.

I recently replaced my aging Red Hat 9 on my home desktop with Ubuntu, and it runs faster than the old Red Hat, everything works, it’s pretty, and there’s just nothing not to like. In the endless religious debate over distros I now take the side of Ubuntu for beginners and even intermediate users who want something that just works.

Changing OSes is no trifling matter for ordinary mortals. The author of the Tipmonkies piece describes himself as having been a “distro whore” trying new Linux distributions all the damn time. Who has spare computers and enough spare hours to squander fucking with those computers? I procrastinated for months before upgrading this box of mine for fear that hair-pulling and struggling with problems would eat up too much time. (Fortunately, Ubuntu is good and I was not unlucky.) But if I had that kind of time, I think I might try to catch up on the hundreds of books and movies I need to read and see, improve my French, learn to scuba dive…

Email rots the brain worse than weed does

+--------------------------------------------------------------------+
| Email Worse Than Marijuana For Intelligence?                       |
|   from the as-an-evergreen-state-graduate-i'm-unqualified-to-commen|
|   posted by Zonk on Friday April 22, @20:35 (The Internet)         |
|   http://slashdot.org/article.pl?sid=05/04/22/2146251              |
+--------------------------------------------------------------------+
wallykeyster writes "The Guardian is reporting that a recent study at
King's College indicates that the [0]average IQ loss of email users was
10 points (or six points more than cannabis users). Details on [1]The
Register as well. The Register has a related story about how [2]computers
make kids dumb and an apparent "problem-solving deficit disorder"
observed in children who use computers. I thought it was television that
rotted your brain?"
Discuss this story at:
http://slashdot.org/comments.pl?sid=05/04/22/2146251
Links:
0. http://www.guardian.co.uk/online/news/0,12597,1465973,00.html
1. http://www.theregister.co.uk/2005/04/22/email_destroys_iq/
2. http://www.theregister.co.uk/2005/03/21/how_dumb_kids/

I suppose it’s not ironic that I picked up this priceless Slashdot tidbit via email, and that I am now wasting my time with this pointless blog entry.

Why I Don’t Use Microsoft Windows

I got my very own first personal computer from Gateway in 1991. It came with Windows 2.0. Then Windows 3.0 came out and I upgraded. Woohoo!
Years rolled by, my computers morphed into different animals as I replaced and upgraded parts, and eventually I upgraded to… Windows 95! Woohoo!
Like a million other consumers I must have assumed that blue screens of death and other mysterious problems were just part of life. People would write funny haikus and stuff, remember?
Around 1996 I started getting interested in making HTML pages. Then I got interested in writing CGI scripts. Then I got interested in web/database programming. I had a shared web hosting account on a Unixoid system and wanted to simulate that environment, more or less, on my development machine and learn more about all that stuff. Over time, I tried several Linux distros and FreeBSD 4.something. Each time I got a little closer to a fully functioning system, but were problems too difficult for me to surmount, and I gave up and stayed with Windows.
Once upon a time, around 2000, I was doing PHP/MySQL development on the computer I had bought from the now-defunct Quantex Systems. It was running the OS that it came with: Windows 98. I was trying to run a reasonable code editor, a browser or two, a MySQL (database) server and Apache (web server). I had a lot of physical memory, but things just kept crashing, and I naively wondered what was wrong.
There is undoubtedly some precise technical explanation of what was wrong, but the high-level, plain English explanation is simple: Windows 98 was a piece of shit junk consumer operating system that was never intended for serious work of the sort I was attempting. I vowed that someday I would leave this abusive relationship and never go back.
I finally did it about three years ago: got a fully functional Linux running. I don’t do Windows anymore, I extremely rarely crash, I can do everything I need to do, and my wife — who is not technically inclined — can sit down in front of it, browse the web, process words, etc., without knowing or caring whether it’s Linux or Windows or something else.
I am never ever going to buy another PC with Windows on it because there is no reason why I should pay for that crap, and neither should you. I have friends and family who struggle with Windows spyware and viruses and I tell them they don’t have to put up with that if they really don’t want to. Nobody is obligated to put up with Windows anymore. You can get a Mac, or a PC with Linux pre-installed from somebody like Monarch Computer, or even <gasp!/> install it yourself and be happy. I’m serious.